From 422383a9e653a3d7b99069b1269d5c3aa9f595cb Mon Sep 17 00:00:00 2001 From: Zaki Shaikh Date: Wed, 10 Jul 2024 15:03:09 +0530 Subject: [PATCH] Add new subcommand auth in tkn-pac CLI Adds new subcommand to authenticate user with git providers and stores those token locally for further use of tkn-pac fix #680 https://issues.redhat.com/browse/SRVKP-5548 Signed-off-by: Zaki Shaikh --- go.mod | 49 +- go.sum | 107 +- pkg/cmd/tknpac/auth/keyring.go | 86 + pkg/cmd/tknpac/auth/login.go | 153 + pkg/cmd/tknpac/auth/login_flow.go | 187 + pkg/cmd/tknpac/auth/root.go | 23 + pkg/cmd/tknpac/auth/success.go | 42 + pkg/cmd/tknpac/root.go | 2 + vendor/github.com/MakeNowJust/heredoc/LICENSE | 21 + .../github.com/MakeNowJust/heredoc/README.md | 52 + .../github.com/MakeNowJust/heredoc/heredoc.go | 105 + .../github.com/alessio/shellescape/.gitignore | 28 + .../alessio/shellescape/.golangci.yml | 59 + .../alessio/shellescape/.goreleaser.yml | 54 + vendor/github.com/alessio/shellescape/AUTHORS | 1 + .../alessio/shellescape/CODE_OF_CONDUCT.md | 76 + vendor/github.com/alessio/shellescape/LICENSE | 21 + .../github.com/alessio/shellescape/README.md | 61 + .../alessio/shellescape/shellescape.go | 66 + .../aymanbagabas/go-osc52/v2/LICENSE | 21 + .../aymanbagabas/go-osc52/v2/README.md | 83 + .../aymanbagabas/go-osc52/v2/osc52.go | 305 ++ .../github.com/briandowns/spinner/.gitignore | 29 + .../github.com/briandowns/spinner/.travis.yml | 18 + vendor/github.com/briandowns/spinner/LICENSE | 174 + vendor/github.com/briandowns/spinner/Makefile | 20 + .../github.com/briandowns/spinner/NOTICE.txt | 4 + .../github.com/briandowns/spinner/README.md | 285 ++ .../briandowns/spinner/character_sets.go | 121 + .../github.com/briandowns/spinner/spinner.go | 447 ++ .../charmbracelet/lipgloss/.gitignore | 1 + .../charmbracelet/lipgloss/.golangci-soft.yml | 47 + .../charmbracelet/lipgloss/.golangci.yml | 29 + .../github.com/charmbracelet/lipgloss/LICENSE | 21 + .../charmbracelet/lipgloss/README.md | 564 +++ .../charmbracelet/lipgloss/align.go | 83 + .../charmbracelet/lipgloss/ansi_unix.go | 7 + .../charmbracelet/lipgloss/ansi_windows.go | 22 + .../charmbracelet/lipgloss/borders.go | 443 ++ .../charmbracelet/lipgloss/color.go | 172 + .../github.com/charmbracelet/lipgloss/get.go | 502 ++ .../github.com/charmbracelet/lipgloss/join.go | 175 + .../charmbracelet/lipgloss/position.go | 154 + .../charmbracelet/lipgloss/renderer.go | 184 + .../charmbracelet/lipgloss/runes.go | 43 + .../github.com/charmbracelet/lipgloss/set.go | 670 +++ .../github.com/charmbracelet/lipgloss/size.go | 41 + .../charmbracelet/lipgloss/style.go | 531 ++ .../charmbracelet/lipgloss/unset.go | 318 ++ .../charmbracelet/lipgloss/whitespace.go | 83 + .../charmbracelet/x/exp/term/LICENSE | 21 + .../charmbracelet/x/exp/term/ansi/ansi.go | 11 + .../charmbracelet/x/exp/term/ansi/ascii.go | 8 + .../x/exp/term/ansi/background.go | 61 + .../charmbracelet/x/exp/term/ansi/c0.go | 72 + .../charmbracelet/x/exp/term/ansi/c1.go | 72 + .../x/exp/term/ansi/clipboard.go | 75 + .../charmbracelet/x/exp/term/ansi/color.go | 191 + .../charmbracelet/x/exp/term/ansi/csi.go | 141 + .../charmbracelet/x/exp/term/ansi/ctrl.go | 17 + .../charmbracelet/x/exp/term/ansi/cursor.go | 190 + .../charmbracelet/x/exp/term/ansi/dcs.go | 148 + .../charmbracelet/x/exp/term/ansi/doc.go | 7 + .../x/exp/term/ansi/hyperlink.go | 28 + .../charmbracelet/x/exp/term/ansi/kitty.go | 58 + .../charmbracelet/x/exp/term/ansi/mode.go | 132 + .../charmbracelet/x/exp/term/ansi/osc.go | 69 + .../charmbracelet/x/exp/term/ansi/params.go | 45 + .../charmbracelet/x/exp/term/ansi/parser.go | 357 ++ .../x/exp/term/ansi/parser/const.go | 78 + .../x/exp/term/ansi/parser/seq.go | 136 + .../exp/term/ansi/parser/transition_table.go | 269 + .../x/exp/term/ansi/passthrough.go | 63 + .../charmbracelet/x/exp/term/ansi/screen.go | 126 + .../charmbracelet/x/exp/term/ansi/sequence.go | 199 + .../charmbracelet/x/exp/term/ansi/style.go | 290 ++ .../charmbracelet/x/exp/term/ansi/termcap.go | 31 + .../charmbracelet/x/exp/term/ansi/title.go | 32 + .../charmbracelet/x/exp/term/ansi/truncate.go | 108 + .../charmbracelet/x/exp/term/ansi/util.go | 29 + .../charmbracelet/x/exp/term/ansi/width.go | 73 + .../charmbracelet/x/exp/term/ansi/wrap.go | 406 ++ .../charmbracelet/x/exp/term/ansi/xterm.go | 33 + vendor/github.com/cli/browser/LICENSE | 23 + vendor/github.com/cli/browser/README.md | 20 + vendor/github.com/cli/browser/browser.go | 57 + .../github.com/cli/browser/browser_darwin.go | 5 + .../github.com/cli/browser/browser_freebsd.go | 15 + .../github.com/cli/browser/browser_linux.go | 21 + .../github.com/cli/browser/browser_netbsd.go | 14 + .../github.com/cli/browser/browser_openbsd.go | 15 + .../cli/browser/browser_unsupported.go | 12 + .../github.com/cli/browser/browser_windows.go | 7 + vendor/github.com/cli/cli/v2/LICENSE | 21 + vendor/github.com/cli/cli/v2/api/client.go | 273 ++ vendor/github.com/cli/cli/v2/api/export_pr.go | 155 + .../github.com/cli/cli/v2/api/export_repo.go | 53 + .../github.com/cli/cli/v2/api/http_client.go | 140 + .../v2/api/queries_branch_issue_reference.go | 145 + .../cli/cli/v2/api/queries_comments.go | 144 + .../cli/cli/v2/api/queries_issue.go | 335 ++ .../github.com/cli/cli/v2/api/queries_org.go | 111 + .../github.com/cli/cli/v2/api/queries_pr.go | 698 +++ .../cli/cli/v2/api/queries_pr_review.go | 117 + .../cli/cli/v2/api/queries_projects_v2.go | 330 ++ .../github.com/cli/cli/v2/api/queries_repo.go | 1390 ++++++ .../github.com/cli/cli/v2/api/queries_user.go | 45 + .../cli/cli/v2/api/query_builder.go | 503 ++ .../cli/cli/v2/api/reaction_groups.go | 59 + .../github.com/cli/cli/v2/context/context.go | 172 + .../github.com/cli/cli/v2/context/remote.go | 123 + vendor/github.com/cli/cli/v2/git/client.go | 726 +++ vendor/github.com/cli/cli/v2/git/command.go | 100 + vendor/github.com/cli/cli/v2/git/errors.go | 39 + vendor/github.com/cli/cli/v2/git/objects.go | 77 + vendor/github.com/cli/cli/v2/git/url.go | 64 + .../cli/cli/v2/internal/authflow/flow.go | 157 + .../cli/cli/v2/internal/authflow/success.go | 42 + .../cli/cli/v2/internal/browser/browser.go | 16 + .../cli/cli/v2/internal/browser/stub.go | 40 + .../cli/cli/v2/internal/config/config.go | 604 +++ .../cli/cli/v2/internal/config/config_mock.go | 592 +++ .../cli/v2/internal/config/migration_mock.go | 149 + .../cli/cli/v2/internal/config/stub.go | 150 + .../cli/cli/v2/internal/ghinstance/host.go | 132 + .../cli/cli/v2/internal/ghrepo/repo.go | 121 + .../cli/cli/v2/internal/keyring/keyring.go | 82 + .../cli/cli/v2/internal/prompter/prompter.go | 125 + .../cli/v2/internal/prompter/prompter_mock.go | 460 ++ .../cli/cli/v2/internal/prompter/test.go | 150 + .../github.com/cli/cli/v2/internal/run/run.go | 98 + .../cli/cli/v2/internal/run/stub.go | 151 + .../v2/pkg/cmd/auth/shared/git_credential.go | 187 + .../cli/v2/pkg/cmd/auth/shared/login_flow.go | 285 ++ .../v2/pkg/cmd/auth/shared/oauth_scopes.go | 106 + .../cli/cli/v2/pkg/cmd/auth/shared/prompt.go | 10 + .../cli/v2/pkg/cmd/auth/shared/writeable.go | 12 + .../cli/cli/v2/pkg/cmd/ssh-key/add/add.go | 107 + .../cli/cli/v2/pkg/cmd/ssh-key/add/http.go | 127 + .../v2/pkg/cmd/ssh-key/shared/user_keys.go | 95 + .../github.com/cli/cli/v2/pkg/cmdutil/args.go | 59 + .../cli/cli/v2/pkg/cmdutil/auth_check.go | 41 + .../cli/cli/v2/pkg/cmdutil/cmdgroup.go | 15 + .../cli/cli/v2/pkg/cmdutil/errors.go | 70 + .../cli/cli/v2/pkg/cmdutil/factory.go | 100 + .../cli/cli/v2/pkg/cmdutil/file_input.go | 16 + .../cli/cli/v2/pkg/cmdutil/flags.go | 182 + .../cli/cli/v2/pkg/cmdutil/json_flags.go | 297 ++ .../cli/cli/v2/pkg/cmdutil/legacy.go | 23 + .../cli/cli/v2/pkg/cmdutil/repo_override.go | 70 + .../cli/cli/v2/pkg/extensions/extension.go | 41 + .../cli/v2/pkg/extensions/extension_mock.go | 400 ++ .../cli/cli/v2/pkg/extensions/manager_mock.go | 406 ++ .../cli/cli/v2/pkg/iostreams/color.go | 226 + .../cli/cli/v2/pkg/iostreams/console.go | 11 + .../cli/v2/pkg/iostreams/console_windows.go | 10 + .../cli/cli/v2/pkg/iostreams/epipe_other.go | 13 + .../cli/cli/v2/pkg/iostreams/epipe_windows.go | 11 + .../cli/cli/v2/pkg/iostreams/iostreams.go | 535 ++ .../cli/cli/v2/pkg/jsoncolor/jsoncolor.go | 113 + .../cli/cli/v2/pkg/set/string_set.go | 84 + .../github.com/cli/cli/v2/pkg/ssh/ssh_keys.go | 113 + .../cli/cli/v2/pkg/surveyext/editor.go | 170 + .../cli/cli/v2/pkg/surveyext/editor_manual.go | 110 + vendor/github.com/cli/cli/v2/utils/utils.go | 37 + vendor/github.com/cli/go-gh/v2/LICENSE | 21 + .../cli/go-gh/v2/internal/git/git.go | 37 + .../cli/go-gh/v2/internal/git/remote.go | 154 + .../cli/go-gh/v2/internal/git/url.go | 83 + .../cli/go-gh/v2/internal/set/string_set.go | 70 + .../cli/go-gh/v2/internal/yamlmap/yaml_map.go | 214 + .../github.com/cli/go-gh/v2/pkg/api/cache.go | 215 + .../cli/go-gh/v2/pkg/api/client_options.go | 106 + .../github.com/cli/go-gh/v2/pkg/api/errors.go | 169 + .../cli/go-gh/v2/pkg/api/graphql_client.go | 182 + .../cli/go-gh/v2/pkg/api/http_client.go | 259 + .../cli/go-gh/v2/pkg/api/log_formatter.go | 47 + .../cli/go-gh/v2/pkg/api/rest_client.go | 170 + .../go-gh/v2/pkg/asciisanitizer/sanitizer.go | 253 + .../github.com/cli/go-gh/v2/pkg/auth/auth.go | 194 + .../cli/go-gh/v2/pkg/browser/browser.go | 80 + .../cli/go-gh/v2/pkg/config/config.go | 336 ++ .../cli/go-gh/v2/pkg/config/errors.go | 32 + vendor/github.com/cli/go-gh/v2/pkg/jq/jq.go | 159 + .../cli/go-gh/v2/pkg/jsonpretty/format.go | 144 + .../cli/go-gh/v2/pkg/prompter/mock.go | 228 + .../cli/go-gh/v2/pkg/prompter/prompter.go | 134 + .../cli/go-gh/v2/pkg/repository/repository.go | 158 + vendor/github.com/cli/go-gh/v2/pkg/ssh/ssh.go | 109 + .../cli/go-gh/v2/pkg/tableprinter/table.go | 259 + .../cli/go-gh/v2/pkg/template/template.go | 261 + .../cli/go-gh/v2/pkg/term/console.go | 17 + .../cli/go-gh/v2/pkg/term/console_windows.go | 22 + .../github.com/cli/go-gh/v2/pkg/term/env.go | 184 + .../github.com/cli/go-gh/v2/pkg/text/text.go | 115 + vendor/github.com/cli/oauth/.golangci.yml | 15 + vendor/github.com/cli/oauth/LICENSE | 21 + vendor/github.com/cli/oauth/README.md | 28 + .../github.com/cli/oauth/api/access_token.go | 27 + vendor/github.com/cli/oauth/api/form.go | 115 + .../cli/oauth/device/device_flow.go | 175 + vendor/github.com/cli/oauth/device/poller.go | 43 + vendor/github.com/cli/oauth/oauth.go | 79 + vendor/github.com/cli/oauth/oauth_device.go | 72 + vendor/github.com/cli/oauth/oauth_webapp.go | 59 + .../cli/oauth/webapp/local_server.go | 85 + .../cli/oauth/webapp/webapp_flow.go | 132 + vendor/github.com/cli/safeexec/LICENSE | 25 + vendor/github.com/cli/safeexec/README.md | 48 + vendor/github.com/cli/safeexec/lookpath.go | 17 + .../github.com/cli/safeexec/lookpath_1.18.go | 10 + .../cli/safeexec/lookpath_windows.go | 120 + .../cli/shurcooL-graphql/.golangci.yml | 11 + .../github.com/cli/shurcooL-graphql/LICENSE | 21 + .../github.com/cli/shurcooL-graphql/README.md | 276 ++ .../cli/shurcooL-graphql/graphql.go | 148 + .../cli/shurcooL-graphql/ident/ident.go | 241 + .../internal/jsonutil/graphql.go | 310 ++ .../github.com/cli/shurcooL-graphql/query.go | 140 + .../github.com/cli/shurcooL-graphql/scalar.go | 51 + .../danieljoos/wincred/.gitattributes | 1 + .../github.com/danieljoos/wincred/.gitignore | 25 + vendor/github.com/danieljoos/wincred/LICENSE | 21 + .../github.com/danieljoos/wincred/README.md | 145 + .../danieljoos/wincred/conversion.go | 116 + .../wincred/conversion_unsupported.go | 11 + vendor/github.com/danieljoos/wincred/sys.go | 148 + .../danieljoos/wincred/sys_unsupported.go | 36 + vendor/github.com/danieljoos/wincred/types.go | 69 + .../github.com/danieljoos/wincred/wincred.go | 114 + vendor/github.com/fatih/color/LICENSE.md | 20 + vendor/github.com/fatih/color/README.md | 176 + vendor/github.com/fatih/color/color.go | 650 +++ .../github.com/fatih/color/color_windows.go | 19 + vendor/github.com/fatih/color/doc.go | 134 + .../github.com/godbus/dbus/v5/CONTRIBUTING.md | 50 + vendor/github.com/godbus/dbus/v5/LICENSE | 25 + vendor/github.com/godbus/dbus/v5/MAINTAINERS | 3 + vendor/github.com/godbus/dbus/v5/README.md | 46 + vendor/github.com/godbus/dbus/v5/auth.go | 257 + .../godbus/dbus/v5/auth_anonymous.go | 16 + .../godbus/dbus/v5/auth_external.go | 26 + vendor/github.com/godbus/dbus/v5/auth_sha1.go | 102 + vendor/github.com/godbus/dbus/v5/call.go | 69 + vendor/github.com/godbus/dbus/v5/conn.go | 996 ++++ .../github.com/godbus/dbus/v5/conn_darwin.go | 37 + .../github.com/godbus/dbus/v5/conn_other.go | 90 + vendor/github.com/godbus/dbus/v5/conn_unix.go | 17 + .../github.com/godbus/dbus/v5/conn_windows.go | 15 + vendor/github.com/godbus/dbus/v5/dbus.go | 430 ++ vendor/github.com/godbus/dbus/v5/decoder.go | 292 ++ .../godbus/dbus/v5/default_handler.go | 342 ++ vendor/github.com/godbus/dbus/v5/doc.go | 71 + vendor/github.com/godbus/dbus/v5/encoder.go | 235 + vendor/github.com/godbus/dbus/v5/escape.go | 84 + vendor/github.com/godbus/dbus/v5/export.go | 463 ++ vendor/github.com/godbus/dbus/v5/homedir.go | 25 + vendor/github.com/godbus/dbus/v5/match.go | 89 + vendor/github.com/godbus/dbus/v5/message.go | 390 ++ vendor/github.com/godbus/dbus/v5/object.go | 174 + vendor/github.com/godbus/dbus/v5/sequence.go | 24 + .../godbus/dbus/v5/sequential_handler.go | 125 + .../godbus/dbus/v5/server_interfaces.go | 107 + vendor/github.com/godbus/dbus/v5/sig.go | 293 ++ .../godbus/dbus/v5/transport_darwin.go | 6 + .../godbus/dbus/v5/transport_generic.go | 52 + .../godbus/dbus/v5/transport_nonce_tcp.go | 39 + .../godbus/dbus/v5/transport_tcp.go | 41 + .../godbus/dbus/v5/transport_unix.go | 212 + .../dbus/v5/transport_unixcred_dragonfly.go | 95 + .../dbus/v5/transport_unixcred_freebsd.go | 92 + .../dbus/v5/transport_unixcred_linux.go | 25 + .../dbus/v5/transport_unixcred_netbsd.go | 14 + .../dbus/v5/transport_unixcred_openbsd.go | 14 + .../godbus/dbus/v5/transport_zos.go | 6 + vendor/github.com/godbus/dbus/v5/variant.go | 150 + .../godbus/dbus/v5/variant_lexer.go | 284 ++ .../godbus/dbus/v5/variant_parser.go | 817 +++ .../go-containerregistry/pkg/name/digest.go | 20 + .../google/shlex/COPYING} | 0 vendor/github.com/google/shlex/README | 2 + vendor/github.com/google/shlex/shlex.go | 416 ++ vendor/github.com/henvic/httpretty/.gitignore | 35 + .../henvic/httpretty/CONTRIBUTING.md | 12 + vendor/github.com/henvic/httpretty/LICENSE.md | 21 + vendor/github.com/henvic/httpretty/README.md | 100 + .../github.com/henvic/httpretty/httpretty.go | 418 ++ .../henvic/httpretty/internal/color/color.go | 164 + .../httpretty/internal/header/header.go | 114 + vendor/github.com/henvic/httpretty/printer.go | 589 +++ .../github.com/henvic/httpretty/recorder.go | 53 + .../henvic/httpretty/testdata/log.txtar | 695 +++ .../henvic/httpretty/testdata/petition.golden | 61 + vendor/github.com/henvic/httpretty/tls.go | 49 + vendor/github.com/itchyny/gojq/.dockerignore | 16 + vendor/github.com/itchyny/gojq/.gitattributes | 3 + vendor/github.com/itchyny/gojq/.gitignore | 7 + vendor/github.com/itchyny/gojq/CHANGELOG.md | 372 ++ vendor/github.com/itchyny/gojq/Dockerfile | 14 + vendor/github.com/itchyny/gojq/LICENSE | 21 + vendor/github.com/itchyny/gojq/Makefile | 103 + vendor/github.com/itchyny/gojq/README.md | 160 + vendor/github.com/itchyny/gojq/_gojq | 43 + vendor/github.com/itchyny/gojq/builtin.go | 69 + vendor/github.com/itchyny/gojq/builtin.jq | 181 + vendor/github.com/itchyny/gojq/code.go | 108 + vendor/github.com/itchyny/gojq/compare.go | 100 + vendor/github.com/itchyny/gojq/compiler.go | 1672 +++++++ vendor/github.com/itchyny/gojq/debug.go | 209 + vendor/github.com/itchyny/gojq/encoder.go | 193 + vendor/github.com/itchyny/gojq/env.go | 48 + vendor/github.com/itchyny/gojq/error.go | 382 ++ vendor/github.com/itchyny/gojq/execute.go | 443 ++ vendor/github.com/itchyny/gojq/func.go | 2168 ++++++++ vendor/github.com/itchyny/gojq/go.dev.mod | 8 + vendor/github.com/itchyny/gojq/go.dev.sum | 4 + vendor/github.com/itchyny/gojq/gojq.go | 5 + vendor/github.com/itchyny/gojq/iter.go | 49 + vendor/github.com/itchyny/gojq/lexer.go | 577 +++ .../github.com/itchyny/gojq/module_loader.go | 182 + vendor/github.com/itchyny/gojq/normalize.go | 84 + vendor/github.com/itchyny/gojq/operator.go | 549 +++ vendor/github.com/itchyny/gojq/option.go | 96 + vendor/github.com/itchyny/gojq/parser.go | 1705 +++++++ vendor/github.com/itchyny/gojq/parser.go.y | 671 +++ vendor/github.com/itchyny/gojq/preview.go | 77 + vendor/github.com/itchyny/gojq/query.go | 1193 +++++ vendor/github.com/itchyny/gojq/release.go | 16 + vendor/github.com/itchyny/gojq/scope_stack.go | 52 + vendor/github.com/itchyny/gojq/stack.go | 56 + vendor/github.com/itchyny/gojq/term_type.go | 77 + vendor/github.com/itchyny/gojq/type.go | 29 + .../itchyny/timefmt-go/CHANGELOG.md | 21 + vendor/github.com/itchyny/timefmt-go/LICENSE | 21 + vendor/github.com/itchyny/timefmt-go/Makefile | 20 + .../github.com/itchyny/timefmt-go/README.md | 69 + .../github.com/itchyny/timefmt-go/format.go | 537 ++ vendor/github.com/itchyny/timefmt-go/parse.go | 408 ++ .../github.com/itchyny/timefmt-go/timefmt.go | 2 + .../lucasb-eyer/go-colorful/.gitignore | 101 + .../lucasb-eyer/go-colorful/CHANGELOG.md | 42 + .../lucasb-eyer/go-colorful/LICENSE | 7 + .../lucasb-eyer/go-colorful/README.md | 482 ++ .../lucasb-eyer/go-colorful/colorgens.go | 55 + .../lucasb-eyer/go-colorful/colors.go | 979 ++++ .../go-colorful/happy_palettegen.go | 25 + .../lucasb-eyer/go-colorful/hexcolor.go | 67 + .../go-colorful/hsluv-snapshot-rev4.json | 1 + .../lucasb-eyer/go-colorful/hsluv.go | 207 + .../go-colorful/soft_palettegen.go | 185 + .../go-colorful/warm_palettegen.go | 25 + vendor/github.com/mattn/go-runewidth/LICENSE | 21 + .../github.com/mattn/go-runewidth/README.md | 27 + .../mattn/go-runewidth/runewidth.go | 358 ++ .../mattn/go-runewidth/runewidth_appengine.go | 9 + .../mattn/go-runewidth/runewidth_js.go | 9 + .../mattn/go-runewidth/runewidth_posix.go | 81 + .../mattn/go-runewidth/runewidth_table.go | 439 ++ .../mattn/go-runewidth/runewidth_windows.go | 28 + vendor/github.com/muesli/reflow/LICENSE | 21 + vendor/github.com/muesli/reflow/ansi/ansi.go | 7 + .../github.com/muesli/reflow/ansi/buffer.go | 40 + .../github.com/muesli/reflow/ansi/writer.go | 76 + .../muesli/reflow/truncate/truncate.go | 120 + vendor/github.com/muesli/termenv/.gitignore | 15 + .../muesli/termenv/.golangci-soft.yml | 47 + .../github.com/muesli/termenv/.golangci.yml | 29 + vendor/github.com/muesli/termenv/LICENSE | 21 + vendor/github.com/muesli/termenv/README.md | 431 ++ .../github.com/muesli/termenv/ansi_compat.md | 65 + .../github.com/muesli/termenv/ansicolors.go | 281 ++ vendor/github.com/muesli/termenv/color.go | 204 + .../muesli/termenv/constants_linux.go | 8 + .../muesli/termenv/constants_solaris.go | 8 + .../muesli/termenv/constants_unix.go | 13 + vendor/github.com/muesli/termenv/copy.go | 37 + vendor/github.com/muesli/termenv/hyperlink.go | 11 + .../github.com/muesli/termenv/notification.go | 11 + vendor/github.com/muesli/termenv/output.go | 197 + vendor/github.com/muesli/termenv/profile.go | 97 + vendor/github.com/muesli/termenv/screen.go | 590 +++ vendor/github.com/muesli/termenv/style.go | 126 + .../muesli/termenv/templatehelper.go | 86 + vendor/github.com/muesli/termenv/termenv.go | 114 + .../muesli/termenv/termenv_other.go | 30 + .../muesli/termenv/termenv_posix.go | 17 + .../muesli/termenv/termenv_solaris.go | 22 + .../github.com/muesli/termenv/termenv_unix.go | 293 ++ .../muesli/termenv/termenv_windows.go | 139 + vendor/github.com/rivo/uniseg/LICENSE.txt | 21 + vendor/github.com/rivo/uniseg/README.md | 137 + vendor/github.com/rivo/uniseg/doc.go | 108 + .../github.com/rivo/uniseg/eastasianwidth.go | 2588 ++++++++++ .../rivo/uniseg/emojipresentation.go | 295 ++ .../github.com/rivo/uniseg/gen_breaktest.go | 215 + .../github.com/rivo/uniseg/gen_properties.go | 261 + vendor/github.com/rivo/uniseg/grapheme.go | 331 ++ .../rivo/uniseg/graphemeproperties.go | 1915 ++++++++ .../github.com/rivo/uniseg/graphemerules.go | 176 + vendor/github.com/rivo/uniseg/line.go | 134 + .../github.com/rivo/uniseg/lineproperties.go | 3554 ++++++++++++++ vendor/github.com/rivo/uniseg/linerules.go | 626 +++ vendor/github.com/rivo/uniseg/properties.go | 208 + vendor/github.com/rivo/uniseg/sentence.go | 90 + .../rivo/uniseg/sentenceproperties.go | 2845 +++++++++++ .../github.com/rivo/uniseg/sentencerules.go | 276 ++ vendor/github.com/rivo/uniseg/step.go | 242 + vendor/github.com/rivo/uniseg/width.go | 61 + vendor/github.com/rivo/uniseg/word.go | 89 + .../github.com/rivo/uniseg/wordproperties.go | 1883 +++++++ vendor/github.com/rivo/uniseg/wordrules.go | 282 ++ vendor/github.com/shurcooL/githubv4/LICENSE | 21 + vendor/github.com/shurcooL/githubv4/README.md | 411 ++ vendor/github.com/shurcooL/githubv4/doc.go | 12 + vendor/github.com/shurcooL/githubv4/enum.go | 2527 ++++++++++ .../github.com/shurcooL/githubv4/githubv4.go | 56 + vendor/github.com/shurcooL/githubv4/input.go | 3755 ++++++++++++++ vendor/github.com/shurcooL/githubv4/scalar.go | 145 + vendor/github.com/shurcooL/graphql/LICENSE | 21 + vendor/github.com/shurcooL/graphql/README.md | 288 ++ vendor/github.com/shurcooL/graphql/doc.go | 8 + vendor/github.com/shurcooL/graphql/graphql.go | 127 + .../shurcooL/graphql/ident/ident.go | 241 + .../graphql/internal/jsonutil/graphql.go | 310 ++ vendor/github.com/shurcooL/graphql/query.go | 131 + vendor/github.com/shurcooL/graphql/scalar.go | 51 + vendor/github.com/spf13/cobra/.golangci.yml | 21 +- vendor/github.com/spf13/cobra/active_help.go | 13 +- vendor/github.com/spf13/cobra/args.go | 4 +- .../spf13/cobra/bash_completions.go | 25 +- vendor/github.com/spf13/cobra/cobra.go | 2 - vendor/github.com/spf13/cobra/command.go | 51 +- vendor/github.com/spf13/cobra/completions.go | 62 +- vendor/github.com/spf13/cobra/flag_groups.go | 34 +- .../spf13/cobra/powershell_completions.go | 4 +- .../thlib/go-timezone-local/LICENSE | 24 + .../thlib/go-timezone-local/tzlocal/tz.go | 42 + .../go-timezone-local/tzlocal/tz_unix.go | 57 + .../go-timezone-local/tzlocal/tz_windows.go | 59 + .../go-timezone-local/tzlocal/tzmapping.go | 707 +++ .../zalando/go-keyring/.catwatch.yml | 1 + .../github.com/zalando/go-keyring/.gitignore | 23 + .../github.com/zalando/go-keyring/.zappr.yml | 8 + .../zalando/go-keyring/CONTRIBUTING.md | 12 + vendor/github.com/zalando/go-keyring/LICENSE | 21 + .../github.com/zalando/go-keyring/MAINTAINERS | 2 + .../github.com/zalando/go-keyring/README.md | 144 + .../github.com/zalando/go-keyring/SECURITY.md | 8 + .../github.com/zalando/go-keyring/keyring.go | 43 + .../zalando/go-keyring/keyring_darwin.go | 118 + .../zalando/go-keyring/keyring_fallback.go | 23 + .../zalando/go-keyring/keyring_mock.go | 62 + .../zalando/go-keyring/keyring_unix.go | 129 + .../zalando/go-keyring/keyring_windows.go | 69 + .../secret_service/secret_service.go | 257 + .../x/crypto/chacha20/chacha_ppc64le.s | 110 +- vendor/golang.org/x/crypto/sha3/sha3_s390x.go | 19 +- vendor/golang.org/x/crypto/ssh/client_auth.go | 14 +- vendor/golang.org/x/crypto/ssh/server.go | 170 +- vendor/golang.org/x/net/html/doc.go | 2 +- .../golang.org/x/net/http/httpguts/httplex.go | 13 +- vendor/golang.org/x/net/http2/frame.go | 13 +- vendor/golang.org/x/net/http2/server.go | 11 +- vendor/golang.org/x/net/http2/transport.go | 15 +- .../x/oauth2/internal/client_appengine.go | 13 - .../golang.org/x/oauth2/internal/transport.go | 5 - vendor/golang.org/x/oauth2/oauth2.go | 2 +- .../golang.org/x/sys/windows/registry/key.go | 205 + .../x/sys/windows/registry/mksyscall.go | 9 + .../x/sys/windows/registry/syscall.go | 32 + .../x/sys/windows/registry/value.go | 386 ++ .../sys/windows/registry/zsyscall_windows.go | 117 + vendor/golang.org/x/text/runes/cond.go | 187 + vendor/golang.org/x/text/runes/runes.go | 355 ++ .../appengine/internal/api.go | 653 --- .../appengine/internal/api_classic.go | 170 - .../appengine/internal/api_common.go | 141 - .../appengine/internal/app_id.go | 28 - .../appengine/internal/base/api_base.pb.go | 308 -- .../appengine/internal/base/api_base.proto | 33 - .../internal/datastore/datastore_v3.pb.go | 4367 ----------------- .../internal/datastore/datastore_v3.proto | 551 --- .../appengine/internal/identity.go | 54 - .../appengine/internal/identity_classic.go | 62 - .../appengine/internal/identity_flex.go | 12 - .../appengine/internal/identity_vm.go | 134 - .../appengine/internal/internal.go | 110 - .../appengine/internal/log/log_service.pb.go | 1313 ----- .../appengine/internal/log/log_service.proto | 150 - .../appengine/internal/main.go | 17 - .../appengine/internal/main_common.go | 7 - .../appengine/internal/main_vm.go | 70 - .../appengine/internal/metadata.go | 60 - .../appengine/internal/net.go | 56 - .../appengine/internal/regen.sh | 40 - .../internal/remote_api/remote_api.pb.go | 361 -- .../internal/remote_api/remote_api.proto | 44 - .../appengine/internal/transaction.go | 115 - .../internal/urlfetch/urlfetch_service.pb.go | 527 -- .../internal/urlfetch/urlfetch_service.proto | 64 - .../appengine/urlfetch/urlfetch.go | 209 - .../protobuf/encoding/protojson/decode.go | 4 +- .../protobuf/encoding/protojson/encode.go | 20 +- .../protobuf/encoding/prototext/decode.go | 4 +- .../protobuf/encoding/prototext/encode.go | 20 +- .../protobuf/internal/descfmt/stringer.go | 1 + .../editiondefaults/editions_defaults.binpb | Bin 63 -> 93 bytes .../internal/editionssupport/editions.go | 13 + .../protobuf/internal/encoding/json/decode.go | 2 +- .../protobuf/internal/encoding/tag/tag.go | 4 +- .../protobuf/internal/encoding/text/decode.go | 2 +- .../protobuf/internal/errors/errors.go | 21 +- .../protobuf/internal/filedesc/desc.go | 88 +- .../protobuf/internal/filedesc/desc_init.go | 43 +- .../protobuf/internal/filedesc/desc_lazy.go | 49 +- .../internal/filedesc/desc_list_gen.go | 11 + .../protobuf/internal/filedesc/editions.go | 22 +- .../protobuf/internal/filedesc/placeholder.go | 1 + .../protobuf/internal/filetype/build.go | 4 +- .../protobuf/internal/genid/descriptor_gen.go | 49 +- .../internal/genid/go_features_gen.go | 2 +- .../protobuf/internal/impl/api_export.go | 6 +- .../protobuf/internal/impl/checkinit.go | 2 +- .../protobuf/internal/impl/codec_extension.go | 22 + .../protobuf/internal/impl/codec_field.go | 64 +- .../protobuf/internal/impl/codec_map.go | 15 +- .../internal/impl/codec_messageset.go | 22 + .../protobuf/internal/impl/convert.go | 2 +- .../protobuf/internal/impl/convert_list.go | 2 +- .../protobuf/internal/impl/convert_map.go | 2 +- .../protobuf/internal/impl/encode.go | 48 +- .../protobuf/internal/impl/extension.go | 8 +- .../protobuf/internal/impl/legacy_enum.go | 3 +- .../internal/impl/legacy_extension.go | 2 +- .../protobuf/internal/impl/legacy_file.go | 4 +- .../protobuf/internal/impl/legacy_message.go | 14 +- .../protobuf/internal/impl/message.go | 8 +- .../protobuf/internal/impl/message_reflect.go | 45 +- .../internal/impl/message_reflect_gen.go | 146 +- .../protobuf/internal/impl/pointer_reflect.go | 6 +- .../protobuf/internal/impl/pointer_unsafe.go | 4 +- .../protobuf/internal/order/range.go | 4 +- .../protobuf/internal/version/version.go | 4 +- .../protobuf/proto/decode.go | 2 + .../protobuf/proto/encode.go | 44 +- .../protobuf/proto/extension.go | 17 +- .../protobuf/proto/messageset.go | 7 +- .../google.golang.org/protobuf/proto/size.go | 2 + .../protobuf/reflect/protodesc/desc.go | 13 +- .../protobuf/reflect/protodesc/desc_init.go | 49 +- .../reflect/protodesc/desc_resolve.go | 5 + .../reflect/protodesc/desc_validate.go | 73 +- .../protobuf/reflect/protodesc/editions.go | 11 +- .../protobuf/reflect/protodesc/proto.go | 22 + .../protobuf/reflect/protoreflect/proto.go | 2 +- .../reflect/protoreflect/source_gen.go | 21 + .../protobuf/reflect/protoreflect/type.go | 12 +- .../reflect/protoreflect/value_pure.go | 14 +- .../reflect/protoreflect/value_union.go | 14 +- .../protoreflect/value_unsafe_go120.go | 6 +- .../protoreflect/value_unsafe_go121.go | 8 +- .../reflect/protoregistry/registry.go | 14 +- .../types/descriptorpb/descriptor.pb.go | 1141 +++-- .../protobuf/types/dynamicpb/dynamic.go | 16 +- .../types/gofeaturespb/go_features.pb.go | 122 +- .../types/gofeaturespb/go_features.proto | 28 - .../protobuf/types/known/anypb/any.pb.go | 4 +- .../types/known/durationpb/duration.pb.go | 4 +- .../protobuf/types/known/emptypb/empty.pb.go | 4 +- .../types/known/fieldmaskpb/field_mask.pb.go | 4 +- .../types/known/structpb/struct.pb.go | 50 +- .../types/known/timestamppb/timestamp.pb.go | 4 +- .../types/known/wrapperspb/wrappers.pb.go | 20 +- vendor/modules.txt | 167 +- 574 files changed, 90039 insertions(+), 10959 deletions(-) create mode 100644 pkg/cmd/tknpac/auth/keyring.go create mode 100644 pkg/cmd/tknpac/auth/login.go create mode 100644 pkg/cmd/tknpac/auth/login_flow.go create mode 100644 pkg/cmd/tknpac/auth/root.go create mode 100644 pkg/cmd/tknpac/auth/success.go create mode 100644 vendor/github.com/MakeNowJust/heredoc/LICENSE create mode 100644 vendor/github.com/MakeNowJust/heredoc/README.md create mode 100644 vendor/github.com/MakeNowJust/heredoc/heredoc.go create mode 100644 vendor/github.com/alessio/shellescape/.gitignore create mode 100644 vendor/github.com/alessio/shellescape/.golangci.yml create mode 100644 vendor/github.com/alessio/shellescape/.goreleaser.yml create mode 100644 vendor/github.com/alessio/shellescape/AUTHORS create mode 100644 vendor/github.com/alessio/shellescape/CODE_OF_CONDUCT.md create mode 100644 vendor/github.com/alessio/shellescape/LICENSE create mode 100644 vendor/github.com/alessio/shellescape/README.md create mode 100644 vendor/github.com/alessio/shellescape/shellescape.go create mode 100644 vendor/github.com/aymanbagabas/go-osc52/v2/LICENSE create mode 100644 vendor/github.com/aymanbagabas/go-osc52/v2/README.md create mode 100644 vendor/github.com/aymanbagabas/go-osc52/v2/osc52.go create mode 100644 vendor/github.com/briandowns/spinner/.gitignore create mode 100644 vendor/github.com/briandowns/spinner/.travis.yml create mode 100644 vendor/github.com/briandowns/spinner/LICENSE create mode 100644 vendor/github.com/briandowns/spinner/Makefile create mode 100644 vendor/github.com/briandowns/spinner/NOTICE.txt create mode 100644 vendor/github.com/briandowns/spinner/README.md create mode 100644 vendor/github.com/briandowns/spinner/character_sets.go create mode 100644 vendor/github.com/briandowns/spinner/spinner.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/.gitignore create mode 100644 vendor/github.com/charmbracelet/lipgloss/.golangci-soft.yml create mode 100644 vendor/github.com/charmbracelet/lipgloss/.golangci.yml create mode 100644 vendor/github.com/charmbracelet/lipgloss/LICENSE create mode 100644 vendor/github.com/charmbracelet/lipgloss/README.md create mode 100644 vendor/github.com/charmbracelet/lipgloss/align.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/ansi_unix.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/ansi_windows.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/borders.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/color.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/get.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/join.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/position.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/renderer.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/runes.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/set.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/size.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/style.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/unset.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/whitespace.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/LICENSE create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/ansi.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/ascii.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/background.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/c0.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/c1.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/clipboard.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/color.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/csi.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/ctrl.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/cursor.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/dcs.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/doc.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/hyperlink.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/kitty.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/mode.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/osc.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/params.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/parser.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/parser/const.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/parser/seq.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/parser/transition_table.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/passthrough.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/screen.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/sequence.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/style.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/termcap.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/title.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/truncate.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/util.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/width.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/wrap.go create mode 100644 vendor/github.com/charmbracelet/x/exp/term/ansi/xterm.go create mode 100644 vendor/github.com/cli/browser/LICENSE create mode 100644 vendor/github.com/cli/browser/README.md create mode 100644 vendor/github.com/cli/browser/browser.go create mode 100644 vendor/github.com/cli/browser/browser_darwin.go create mode 100644 vendor/github.com/cli/browser/browser_freebsd.go create mode 100644 vendor/github.com/cli/browser/browser_linux.go create mode 100644 vendor/github.com/cli/browser/browser_netbsd.go create mode 100644 vendor/github.com/cli/browser/browser_openbsd.go create mode 100644 vendor/github.com/cli/browser/browser_unsupported.go create mode 100644 vendor/github.com/cli/browser/browser_windows.go create mode 100644 vendor/github.com/cli/cli/v2/LICENSE create mode 100644 vendor/github.com/cli/cli/v2/api/client.go create mode 100644 vendor/github.com/cli/cli/v2/api/export_pr.go create mode 100644 vendor/github.com/cli/cli/v2/api/export_repo.go create mode 100644 vendor/github.com/cli/cli/v2/api/http_client.go create mode 100644 vendor/github.com/cli/cli/v2/api/queries_branch_issue_reference.go create mode 100644 vendor/github.com/cli/cli/v2/api/queries_comments.go create mode 100644 vendor/github.com/cli/cli/v2/api/queries_issue.go create mode 100644 vendor/github.com/cli/cli/v2/api/queries_org.go create mode 100644 vendor/github.com/cli/cli/v2/api/queries_pr.go create mode 100644 vendor/github.com/cli/cli/v2/api/queries_pr_review.go create mode 100644 vendor/github.com/cli/cli/v2/api/queries_projects_v2.go create mode 100644 vendor/github.com/cli/cli/v2/api/queries_repo.go create mode 100644 vendor/github.com/cli/cli/v2/api/queries_user.go create mode 100644 vendor/github.com/cli/cli/v2/api/query_builder.go create mode 100644 vendor/github.com/cli/cli/v2/api/reaction_groups.go create mode 100644 vendor/github.com/cli/cli/v2/context/context.go create mode 100644 vendor/github.com/cli/cli/v2/context/remote.go create mode 100644 vendor/github.com/cli/cli/v2/git/client.go create mode 100644 vendor/github.com/cli/cli/v2/git/command.go create mode 100644 vendor/github.com/cli/cli/v2/git/errors.go create mode 100644 vendor/github.com/cli/cli/v2/git/objects.go create mode 100644 vendor/github.com/cli/cli/v2/git/url.go create mode 100644 vendor/github.com/cli/cli/v2/internal/authflow/flow.go create mode 100644 vendor/github.com/cli/cli/v2/internal/authflow/success.go create mode 100644 vendor/github.com/cli/cli/v2/internal/browser/browser.go create mode 100644 vendor/github.com/cli/cli/v2/internal/browser/stub.go create mode 100644 vendor/github.com/cli/cli/v2/internal/config/config.go create mode 100644 vendor/github.com/cli/cli/v2/internal/config/config_mock.go create mode 100644 vendor/github.com/cli/cli/v2/internal/config/migration_mock.go create mode 100644 vendor/github.com/cli/cli/v2/internal/config/stub.go create mode 100644 vendor/github.com/cli/cli/v2/internal/ghinstance/host.go create mode 100644 vendor/github.com/cli/cli/v2/internal/ghrepo/repo.go create mode 100644 vendor/github.com/cli/cli/v2/internal/keyring/keyring.go create mode 100644 vendor/github.com/cli/cli/v2/internal/prompter/prompter.go create mode 100644 vendor/github.com/cli/cli/v2/internal/prompter/prompter_mock.go create mode 100644 vendor/github.com/cli/cli/v2/internal/prompter/test.go create mode 100644 vendor/github.com/cli/cli/v2/internal/run/run.go create mode 100644 vendor/github.com/cli/cli/v2/internal/run/stub.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/cmd/auth/shared/git_credential.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/cmd/auth/shared/login_flow.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/cmd/auth/shared/oauth_scopes.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/cmd/auth/shared/prompt.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/cmd/auth/shared/writeable.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/cmd/ssh-key/add/add.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/cmd/ssh-key/add/http.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/cmd/ssh-key/shared/user_keys.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/cmdutil/args.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/cmdutil/auth_check.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/cmdutil/cmdgroup.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/cmdutil/errors.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/cmdutil/factory.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/cmdutil/file_input.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/cmdutil/flags.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/cmdutil/json_flags.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/cmdutil/legacy.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/cmdutil/repo_override.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/extensions/extension.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/extensions/extension_mock.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/extensions/manager_mock.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/iostreams/color.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/iostreams/console.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/iostreams/console_windows.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/iostreams/epipe_other.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/iostreams/epipe_windows.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/iostreams/iostreams.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/jsoncolor/jsoncolor.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/set/string_set.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/ssh/ssh_keys.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/surveyext/editor.go create mode 100644 vendor/github.com/cli/cli/v2/pkg/surveyext/editor_manual.go create mode 100644 vendor/github.com/cli/cli/v2/utils/utils.go create mode 100644 vendor/github.com/cli/go-gh/v2/LICENSE create mode 100644 vendor/github.com/cli/go-gh/v2/internal/git/git.go create mode 100644 vendor/github.com/cli/go-gh/v2/internal/git/remote.go create mode 100644 vendor/github.com/cli/go-gh/v2/internal/git/url.go create mode 100644 vendor/github.com/cli/go-gh/v2/internal/set/string_set.go create mode 100644 vendor/github.com/cli/go-gh/v2/internal/yamlmap/yaml_map.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/api/cache.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/api/client_options.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/api/errors.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/api/graphql_client.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/api/http_client.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/api/log_formatter.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/api/rest_client.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/asciisanitizer/sanitizer.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/auth/auth.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/browser/browser.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/config/config.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/config/errors.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/jq/jq.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/jsonpretty/format.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/prompter/mock.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/prompter/prompter.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/repository/repository.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/ssh/ssh.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/tableprinter/table.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/template/template.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/term/console.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/term/console_windows.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/term/env.go create mode 100644 vendor/github.com/cli/go-gh/v2/pkg/text/text.go create mode 100644 vendor/github.com/cli/oauth/.golangci.yml create mode 100644 vendor/github.com/cli/oauth/LICENSE create mode 100644 vendor/github.com/cli/oauth/README.md create mode 100644 vendor/github.com/cli/oauth/api/access_token.go create mode 100644 vendor/github.com/cli/oauth/api/form.go create mode 100644 vendor/github.com/cli/oauth/device/device_flow.go create mode 100644 vendor/github.com/cli/oauth/device/poller.go create mode 100644 vendor/github.com/cli/oauth/oauth.go create mode 100644 vendor/github.com/cli/oauth/oauth_device.go create mode 100644 vendor/github.com/cli/oauth/oauth_webapp.go create mode 100644 vendor/github.com/cli/oauth/webapp/local_server.go create mode 100644 vendor/github.com/cli/oauth/webapp/webapp_flow.go create mode 100644 vendor/github.com/cli/safeexec/LICENSE create mode 100644 vendor/github.com/cli/safeexec/README.md create mode 100644 vendor/github.com/cli/safeexec/lookpath.go create mode 100644 vendor/github.com/cli/safeexec/lookpath_1.18.go create mode 100644 vendor/github.com/cli/safeexec/lookpath_windows.go create mode 100644 vendor/github.com/cli/shurcooL-graphql/.golangci.yml create mode 100644 vendor/github.com/cli/shurcooL-graphql/LICENSE create mode 100644 vendor/github.com/cli/shurcooL-graphql/README.md create mode 100644 vendor/github.com/cli/shurcooL-graphql/graphql.go create mode 100644 vendor/github.com/cli/shurcooL-graphql/ident/ident.go create mode 100644 vendor/github.com/cli/shurcooL-graphql/internal/jsonutil/graphql.go create mode 100644 vendor/github.com/cli/shurcooL-graphql/query.go create mode 100644 vendor/github.com/cli/shurcooL-graphql/scalar.go create mode 100644 vendor/github.com/danieljoos/wincred/.gitattributes create mode 100644 vendor/github.com/danieljoos/wincred/.gitignore create mode 100644 vendor/github.com/danieljoos/wincred/LICENSE create mode 100644 vendor/github.com/danieljoos/wincred/README.md create mode 100644 vendor/github.com/danieljoos/wincred/conversion.go create mode 100644 vendor/github.com/danieljoos/wincred/conversion_unsupported.go create mode 100644 vendor/github.com/danieljoos/wincred/sys.go create mode 100644 vendor/github.com/danieljoos/wincred/sys_unsupported.go create mode 100644 vendor/github.com/danieljoos/wincred/types.go create mode 100644 vendor/github.com/danieljoos/wincred/wincred.go create mode 100644 vendor/github.com/fatih/color/LICENSE.md create mode 100644 vendor/github.com/fatih/color/README.md create mode 100644 vendor/github.com/fatih/color/color.go create mode 100644 vendor/github.com/fatih/color/color_windows.go create mode 100644 vendor/github.com/fatih/color/doc.go create mode 100644 vendor/github.com/godbus/dbus/v5/CONTRIBUTING.md create mode 100644 vendor/github.com/godbus/dbus/v5/LICENSE create mode 100644 vendor/github.com/godbus/dbus/v5/MAINTAINERS create mode 100644 vendor/github.com/godbus/dbus/v5/README.md create mode 100644 vendor/github.com/godbus/dbus/v5/auth.go create mode 100644 vendor/github.com/godbus/dbus/v5/auth_anonymous.go create mode 100644 vendor/github.com/godbus/dbus/v5/auth_external.go create mode 100644 vendor/github.com/godbus/dbus/v5/auth_sha1.go create mode 100644 vendor/github.com/godbus/dbus/v5/call.go create mode 100644 vendor/github.com/godbus/dbus/v5/conn.go create mode 100644 vendor/github.com/godbus/dbus/v5/conn_darwin.go create mode 100644 vendor/github.com/godbus/dbus/v5/conn_other.go create mode 100644 vendor/github.com/godbus/dbus/v5/conn_unix.go create mode 100644 vendor/github.com/godbus/dbus/v5/conn_windows.go create mode 100644 vendor/github.com/godbus/dbus/v5/dbus.go create mode 100644 vendor/github.com/godbus/dbus/v5/decoder.go create mode 100644 vendor/github.com/godbus/dbus/v5/default_handler.go create mode 100644 vendor/github.com/godbus/dbus/v5/doc.go create mode 100644 vendor/github.com/godbus/dbus/v5/encoder.go create mode 100644 vendor/github.com/godbus/dbus/v5/escape.go create mode 100644 vendor/github.com/godbus/dbus/v5/export.go create mode 100644 vendor/github.com/godbus/dbus/v5/homedir.go create mode 100644 vendor/github.com/godbus/dbus/v5/match.go create mode 100644 vendor/github.com/godbus/dbus/v5/message.go create mode 100644 vendor/github.com/godbus/dbus/v5/object.go create mode 100644 vendor/github.com/godbus/dbus/v5/sequence.go create mode 100644 vendor/github.com/godbus/dbus/v5/sequential_handler.go create mode 100644 vendor/github.com/godbus/dbus/v5/server_interfaces.go create mode 100644 vendor/github.com/godbus/dbus/v5/sig.go create mode 100644 vendor/github.com/godbus/dbus/v5/transport_darwin.go create mode 100644 vendor/github.com/godbus/dbus/v5/transport_generic.go create mode 100644 vendor/github.com/godbus/dbus/v5/transport_nonce_tcp.go create mode 100644 vendor/github.com/godbus/dbus/v5/transport_tcp.go create mode 100644 vendor/github.com/godbus/dbus/v5/transport_unix.go create mode 100644 vendor/github.com/godbus/dbus/v5/transport_unixcred_dragonfly.go create mode 100644 vendor/github.com/godbus/dbus/v5/transport_unixcred_freebsd.go create mode 100644 vendor/github.com/godbus/dbus/v5/transport_unixcred_linux.go create mode 100644 vendor/github.com/godbus/dbus/v5/transport_unixcred_netbsd.go create mode 100644 vendor/github.com/godbus/dbus/v5/transport_unixcred_openbsd.go create mode 100644 vendor/github.com/godbus/dbus/v5/transport_zos.go create mode 100644 vendor/github.com/godbus/dbus/v5/variant.go create mode 100644 vendor/github.com/godbus/dbus/v5/variant_lexer.go create mode 100644 vendor/github.com/godbus/dbus/v5/variant_parser.go rename vendor/{google.golang.org/appengine/LICENSE => github.com/google/shlex/COPYING} (100%) create mode 100644 vendor/github.com/google/shlex/README create mode 100644 vendor/github.com/google/shlex/shlex.go create mode 100644 vendor/github.com/henvic/httpretty/.gitignore create mode 100644 vendor/github.com/henvic/httpretty/CONTRIBUTING.md create mode 100644 vendor/github.com/henvic/httpretty/LICENSE.md create mode 100644 vendor/github.com/henvic/httpretty/README.md create mode 100644 vendor/github.com/henvic/httpretty/httpretty.go create mode 100644 vendor/github.com/henvic/httpretty/internal/color/color.go create mode 100644 vendor/github.com/henvic/httpretty/internal/header/header.go create mode 100644 vendor/github.com/henvic/httpretty/printer.go create mode 100644 vendor/github.com/henvic/httpretty/recorder.go create mode 100644 vendor/github.com/henvic/httpretty/testdata/log.txtar create mode 100644 vendor/github.com/henvic/httpretty/testdata/petition.golden create mode 100644 vendor/github.com/henvic/httpretty/tls.go create mode 100644 vendor/github.com/itchyny/gojq/.dockerignore create mode 100644 vendor/github.com/itchyny/gojq/.gitattributes create mode 100644 vendor/github.com/itchyny/gojq/.gitignore create mode 100644 vendor/github.com/itchyny/gojq/CHANGELOG.md create mode 100644 vendor/github.com/itchyny/gojq/Dockerfile create mode 100644 vendor/github.com/itchyny/gojq/LICENSE create mode 100644 vendor/github.com/itchyny/gojq/Makefile create mode 100644 vendor/github.com/itchyny/gojq/README.md create mode 100644 vendor/github.com/itchyny/gojq/_gojq create mode 100644 vendor/github.com/itchyny/gojq/builtin.go create mode 100644 vendor/github.com/itchyny/gojq/builtin.jq create mode 100644 vendor/github.com/itchyny/gojq/code.go create mode 100644 vendor/github.com/itchyny/gojq/compare.go create mode 100644 vendor/github.com/itchyny/gojq/compiler.go create mode 100644 vendor/github.com/itchyny/gojq/debug.go create mode 100644 vendor/github.com/itchyny/gojq/encoder.go create mode 100644 vendor/github.com/itchyny/gojq/env.go create mode 100644 vendor/github.com/itchyny/gojq/error.go create mode 100644 vendor/github.com/itchyny/gojq/execute.go create mode 100644 vendor/github.com/itchyny/gojq/func.go create mode 100644 vendor/github.com/itchyny/gojq/go.dev.mod create mode 100644 vendor/github.com/itchyny/gojq/go.dev.sum create mode 100644 vendor/github.com/itchyny/gojq/gojq.go create mode 100644 vendor/github.com/itchyny/gojq/iter.go create mode 100644 vendor/github.com/itchyny/gojq/lexer.go create mode 100644 vendor/github.com/itchyny/gojq/module_loader.go create mode 100644 vendor/github.com/itchyny/gojq/normalize.go create mode 100644 vendor/github.com/itchyny/gojq/operator.go create mode 100644 vendor/github.com/itchyny/gojq/option.go create mode 100644 vendor/github.com/itchyny/gojq/parser.go create mode 100644 vendor/github.com/itchyny/gojq/parser.go.y create mode 100644 vendor/github.com/itchyny/gojq/preview.go create mode 100644 vendor/github.com/itchyny/gojq/query.go create mode 100644 vendor/github.com/itchyny/gojq/release.go create mode 100644 vendor/github.com/itchyny/gojq/scope_stack.go create mode 100644 vendor/github.com/itchyny/gojq/stack.go create mode 100644 vendor/github.com/itchyny/gojq/term_type.go create mode 100644 vendor/github.com/itchyny/gojq/type.go create mode 100644 vendor/github.com/itchyny/timefmt-go/CHANGELOG.md create mode 100644 vendor/github.com/itchyny/timefmt-go/LICENSE create mode 100644 vendor/github.com/itchyny/timefmt-go/Makefile create mode 100644 vendor/github.com/itchyny/timefmt-go/README.md create mode 100644 vendor/github.com/itchyny/timefmt-go/format.go create mode 100644 vendor/github.com/itchyny/timefmt-go/parse.go create mode 100644 vendor/github.com/itchyny/timefmt-go/timefmt.go create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/.gitignore create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/CHANGELOG.md create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/LICENSE create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/README.md create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/colorgens.go create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/colors.go create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/happy_palettegen.go create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/hexcolor.go create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/hsluv-snapshot-rev4.json create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/hsluv.go create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/soft_palettegen.go create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/warm_palettegen.go create mode 100644 vendor/github.com/mattn/go-runewidth/LICENSE create mode 100644 vendor/github.com/mattn/go-runewidth/README.md create mode 100644 vendor/github.com/mattn/go-runewidth/runewidth.go create mode 100644 vendor/github.com/mattn/go-runewidth/runewidth_appengine.go create mode 100644 vendor/github.com/mattn/go-runewidth/runewidth_js.go create mode 100644 vendor/github.com/mattn/go-runewidth/runewidth_posix.go create mode 100644 vendor/github.com/mattn/go-runewidth/runewidth_table.go create mode 100644 vendor/github.com/mattn/go-runewidth/runewidth_windows.go create mode 100644 vendor/github.com/muesli/reflow/LICENSE create mode 100644 vendor/github.com/muesli/reflow/ansi/ansi.go create mode 100644 vendor/github.com/muesli/reflow/ansi/buffer.go create mode 100644 vendor/github.com/muesli/reflow/ansi/writer.go create mode 100644 vendor/github.com/muesli/reflow/truncate/truncate.go create mode 100644 vendor/github.com/muesli/termenv/.gitignore create mode 100644 vendor/github.com/muesli/termenv/.golangci-soft.yml create mode 100644 vendor/github.com/muesli/termenv/.golangci.yml create mode 100644 vendor/github.com/muesli/termenv/LICENSE create mode 100644 vendor/github.com/muesli/termenv/README.md create mode 100644 vendor/github.com/muesli/termenv/ansi_compat.md create mode 100644 vendor/github.com/muesli/termenv/ansicolors.go create mode 100644 vendor/github.com/muesli/termenv/color.go create mode 100644 vendor/github.com/muesli/termenv/constants_linux.go create mode 100644 vendor/github.com/muesli/termenv/constants_solaris.go create mode 100644 vendor/github.com/muesli/termenv/constants_unix.go create mode 100644 vendor/github.com/muesli/termenv/copy.go create mode 100644 vendor/github.com/muesli/termenv/hyperlink.go create mode 100644 vendor/github.com/muesli/termenv/notification.go create mode 100644 vendor/github.com/muesli/termenv/output.go create mode 100644 vendor/github.com/muesli/termenv/profile.go create mode 100644 vendor/github.com/muesli/termenv/screen.go create mode 100644 vendor/github.com/muesli/termenv/style.go create mode 100644 vendor/github.com/muesli/termenv/templatehelper.go create mode 100644 vendor/github.com/muesli/termenv/termenv.go create mode 100644 vendor/github.com/muesli/termenv/termenv_other.go create mode 100644 vendor/github.com/muesli/termenv/termenv_posix.go create mode 100644 vendor/github.com/muesli/termenv/termenv_solaris.go create mode 100644 vendor/github.com/muesli/termenv/termenv_unix.go create mode 100644 vendor/github.com/muesli/termenv/termenv_windows.go create mode 100644 vendor/github.com/rivo/uniseg/LICENSE.txt create mode 100644 vendor/github.com/rivo/uniseg/README.md create mode 100644 vendor/github.com/rivo/uniseg/doc.go create mode 100644 vendor/github.com/rivo/uniseg/eastasianwidth.go create mode 100644 vendor/github.com/rivo/uniseg/emojipresentation.go create mode 100644 vendor/github.com/rivo/uniseg/gen_breaktest.go create mode 100644 vendor/github.com/rivo/uniseg/gen_properties.go create mode 100644 vendor/github.com/rivo/uniseg/grapheme.go create mode 100644 vendor/github.com/rivo/uniseg/graphemeproperties.go create mode 100644 vendor/github.com/rivo/uniseg/graphemerules.go create mode 100644 vendor/github.com/rivo/uniseg/line.go create mode 100644 vendor/github.com/rivo/uniseg/lineproperties.go create mode 100644 vendor/github.com/rivo/uniseg/linerules.go create mode 100644 vendor/github.com/rivo/uniseg/properties.go create mode 100644 vendor/github.com/rivo/uniseg/sentence.go create mode 100644 vendor/github.com/rivo/uniseg/sentenceproperties.go create mode 100644 vendor/github.com/rivo/uniseg/sentencerules.go create mode 100644 vendor/github.com/rivo/uniseg/step.go create mode 100644 vendor/github.com/rivo/uniseg/width.go create mode 100644 vendor/github.com/rivo/uniseg/word.go create mode 100644 vendor/github.com/rivo/uniseg/wordproperties.go create mode 100644 vendor/github.com/rivo/uniseg/wordrules.go create mode 100644 vendor/github.com/shurcooL/githubv4/LICENSE create mode 100644 vendor/github.com/shurcooL/githubv4/README.md create mode 100644 vendor/github.com/shurcooL/githubv4/doc.go create mode 100644 vendor/github.com/shurcooL/githubv4/enum.go create mode 100644 vendor/github.com/shurcooL/githubv4/githubv4.go create mode 100644 vendor/github.com/shurcooL/githubv4/input.go create mode 100644 vendor/github.com/shurcooL/githubv4/scalar.go create mode 100644 vendor/github.com/shurcooL/graphql/LICENSE create mode 100644 vendor/github.com/shurcooL/graphql/README.md create mode 100644 vendor/github.com/shurcooL/graphql/doc.go create mode 100644 vendor/github.com/shurcooL/graphql/graphql.go create mode 100644 vendor/github.com/shurcooL/graphql/ident/ident.go create mode 100644 vendor/github.com/shurcooL/graphql/internal/jsonutil/graphql.go create mode 100644 vendor/github.com/shurcooL/graphql/query.go create mode 100644 vendor/github.com/shurcooL/graphql/scalar.go create mode 100644 vendor/github.com/thlib/go-timezone-local/LICENSE create mode 100644 vendor/github.com/thlib/go-timezone-local/tzlocal/tz.go create mode 100644 vendor/github.com/thlib/go-timezone-local/tzlocal/tz_unix.go create mode 100644 vendor/github.com/thlib/go-timezone-local/tzlocal/tz_windows.go create mode 100644 vendor/github.com/thlib/go-timezone-local/tzlocal/tzmapping.go create mode 100644 vendor/github.com/zalando/go-keyring/.catwatch.yml create mode 100644 vendor/github.com/zalando/go-keyring/.gitignore create mode 100644 vendor/github.com/zalando/go-keyring/.zappr.yml create mode 100644 vendor/github.com/zalando/go-keyring/CONTRIBUTING.md create mode 100644 vendor/github.com/zalando/go-keyring/LICENSE create mode 100644 vendor/github.com/zalando/go-keyring/MAINTAINERS create mode 100644 vendor/github.com/zalando/go-keyring/README.md create mode 100644 vendor/github.com/zalando/go-keyring/SECURITY.md create mode 100644 vendor/github.com/zalando/go-keyring/keyring.go create mode 100644 vendor/github.com/zalando/go-keyring/keyring_darwin.go create mode 100644 vendor/github.com/zalando/go-keyring/keyring_fallback.go create mode 100644 vendor/github.com/zalando/go-keyring/keyring_mock.go create mode 100644 vendor/github.com/zalando/go-keyring/keyring_unix.go create mode 100644 vendor/github.com/zalando/go-keyring/keyring_windows.go create mode 100644 vendor/github.com/zalando/go-keyring/secret_service/secret_service.go delete mode 100644 vendor/golang.org/x/oauth2/internal/client_appengine.go create mode 100644 vendor/golang.org/x/sys/windows/registry/key.go create mode 100644 vendor/golang.org/x/sys/windows/registry/mksyscall.go create mode 100644 vendor/golang.org/x/sys/windows/registry/syscall.go create mode 100644 vendor/golang.org/x/sys/windows/registry/value.go create mode 100644 vendor/golang.org/x/sys/windows/registry/zsyscall_windows.go create mode 100644 vendor/golang.org/x/text/runes/cond.go create mode 100644 vendor/golang.org/x/text/runes/runes.go delete mode 100644 vendor/google.golang.org/appengine/internal/api.go delete mode 100644 vendor/google.golang.org/appengine/internal/api_classic.go delete mode 100644 vendor/google.golang.org/appengine/internal/api_common.go delete mode 100644 vendor/google.golang.org/appengine/internal/app_id.go delete mode 100644 vendor/google.golang.org/appengine/internal/base/api_base.pb.go delete mode 100644 vendor/google.golang.org/appengine/internal/base/api_base.proto delete mode 100644 vendor/google.golang.org/appengine/internal/datastore/datastore_v3.pb.go delete mode 100644 vendor/google.golang.org/appengine/internal/datastore/datastore_v3.proto delete mode 100644 vendor/google.golang.org/appengine/internal/identity.go delete mode 100644 vendor/google.golang.org/appengine/internal/identity_classic.go delete mode 100644 vendor/google.golang.org/appengine/internal/identity_flex.go delete mode 100644 vendor/google.golang.org/appengine/internal/identity_vm.go delete mode 100644 vendor/google.golang.org/appengine/internal/internal.go delete mode 100644 vendor/google.golang.org/appengine/internal/log/log_service.pb.go delete mode 100644 vendor/google.golang.org/appengine/internal/log/log_service.proto delete mode 100644 vendor/google.golang.org/appengine/internal/main.go delete mode 100644 vendor/google.golang.org/appengine/internal/main_common.go delete mode 100644 vendor/google.golang.org/appengine/internal/main_vm.go delete mode 100644 vendor/google.golang.org/appengine/internal/metadata.go delete mode 100644 vendor/google.golang.org/appengine/internal/net.go delete mode 100644 vendor/google.golang.org/appengine/internal/regen.sh delete mode 100644 vendor/google.golang.org/appengine/internal/remote_api/remote_api.pb.go delete mode 100644 vendor/google.golang.org/appengine/internal/remote_api/remote_api.proto delete mode 100644 vendor/google.golang.org/appengine/internal/transaction.go delete mode 100644 vendor/google.golang.org/appengine/internal/urlfetch/urlfetch_service.pb.go delete mode 100644 vendor/google.golang.org/appengine/internal/urlfetch/urlfetch_service.proto delete mode 100644 vendor/google.golang.org/appengine/urlfetch/urlfetch.go create mode 100644 vendor/google.golang.org/protobuf/internal/editionssupport/editions.go delete mode 100644 vendor/google.golang.org/protobuf/types/gofeaturespb/go_features.proto diff --git a/go.mod b/go.mod index f3cf087f9..0d107c738 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,8 @@ require ( code.gitea.io/sdk/gitea v0.17.1 github.com/AlecAivazis/survey/v2 v2.3.7 github.com/bradleyfalzon/ghinstallation/v2 v2.10.0 + github.com/cli/cli/v2 v2.46.0 + github.com/cli/oauth v1.0.1 github.com/cloudevents/sdk-go/v2 v2.15.2 github.com/fvbommel/sortorder v1.1.0 github.com/gfleury/go-bitbucket-v1 v0.0.0-20240131155556-0b41d7863037 @@ -25,16 +27,17 @@ require ( github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d github.com/mitchellh/mapstructure v1.5.0 github.com/pkg/errors v0.9.1 - github.com/spf13/cobra v1.8.0 + github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 github.com/tektoncd/pipeline v0.58.0 github.com/xanzy/go-gitlab v0.101.0 + github.com/zalando/go-keyring v0.2.5 go.opencensus.io v0.24.0 go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 - golang.org/x/oauth2 v0.18.0 + golang.org/x/oauth2 v0.21.0 golang.org/x/sync v0.6.0 - golang.org/x/text v0.14.0 + golang.org/x/text v0.15.0 gopkg.in/yaml.v2 v2.4.0 gotest.tools/v3 v3.5.1 k8s.io/api v0.29.3 @@ -47,8 +50,33 @@ require ( ) require ( + github.com/MakeNowJust/heredoc v1.0.0 // indirect + github.com/alessio/shellescape v1.4.2 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/briandowns/spinner v1.18.1 // indirect + github.com/charmbracelet/lipgloss v0.10.1-0.20240413172830-d0be07ea6b9c // indirect + github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f // indirect + github.com/cli/browser v1.3.0 // indirect + github.com/cli/go-gh/v2 v2.9.0 // indirect + github.com/cli/safeexec v1.0.1 // indirect + github.com/cli/shurcooL-graphql v0.0.4 // indirect github.com/coreos/go-oidc/v3 v3.9.0 // indirect + github.com/danieljoos/wincred v1.2.1 // indirect + github.com/fatih/color v1.16.0 // indirect github.com/go-jose/go-jose/v3 v3.0.3 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/henvic/httpretty v0.1.3 // indirect + github.com/itchyny/gojq v0.12.15 // indirect + github.com/itchyny/timefmt-go v0.1.5 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/shurcooL/githubv4 v0.0.0-20230704064427-599ae7bbf278 // indirect + github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect + github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect ) replace ( @@ -75,7 +103,7 @@ require ( github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cloudevents/sdk-go/observability/opencensus/v2 v2.15.2 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidmz/go-pageant v1.0.2 // indirect github.com/emicklei/go-restful/v3 v3.12.0 // indirect github.com/evanphx/json-patch v5.9.0+incompatible // indirect @@ -93,7 +121,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic v0.7.0 // indirect github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect - github.com/google/go-containerregistry v0.19.1 // indirect + github.com/google/go-containerregistry v0.19.2 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20230111200839-76d1ae5aea2b // indirect @@ -118,7 +146,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/openzipkin/zipkin-go v0.4.2 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.19.0 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.52.2 // indirect @@ -131,18 +159,17 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/automaxprocs v1.5.3 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.21.0 // indirect - golang.org/x/net v0.23.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect golang.org/x/sys v0.20.0 // indirect - golang.org/x/term v0.18.0 // indirect + golang.org/x/term v0.20.0 // indirect golang.org/x/time v0.5.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/api v0.172.0 // indirect - google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect google.golang.org/grpc v1.63.0 // indirect - google.golang.org/protobuf v1.33.0 + google.golang.org/protobuf v1.34.2 gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.29.3 // indirect diff --git a/go.sum b/go.sum index e473ddf0c..3b53ce5fa 100644 --- a/go.sum +++ b/go.sum @@ -612,6 +612,8 @@ github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1r github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -629,6 +631,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= +github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= @@ -640,6 +644,8 @@ github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9 github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -652,6 +658,8 @@ github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bradleyfalzon/ghinstallation/v2 v2.10.0 h1:XWuWBRFEpqVrHepQob9yPS3Xg4K3Wr9QCx4fu8HbUNg= github.com/bradleyfalzon/ghinstallation/v2 v2.10.0/go.mod h1:qoGA4DxWPaYTgVCrmEspVSjlTu4WYAiSxMIhorMRXXc= +github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY= +github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -662,9 +670,27 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/lipgloss v0.10.1-0.20240413172830-d0be07ea6b9c h1:0FwZb0wTiyalb8QQlILWyIuh3nF5wok6j9D9oUQwfQY= +github.com/charmbracelet/lipgloss v0.10.1-0.20240413172830-d0be07ea6b9c/go.mod h1:EPP2QJ0ectp3zo6gx9f8oJGq8keirqPJ3XpYEI8wrrs= +github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f h1:1BXkZqDueTOBECyDoFGRi0xMYgjJ6vvoPIkWyKOwzTc= +github.com/charmbracelet/x/exp/term v0.0.0-20240425164147-ba2a9512b05f/go.mod h1:yQqGHmheaQfkqiJWjklPHVAq1dKbk8uGbcoS/lcKCJ0= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cli/browser v1.0.0/go.mod h1:IEWkHYbLjkhtjwwWlwTHW2lGxeS5gezEQBMLTwDHf5Q= +github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= +github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= +github.com/cli/cli/v2 v2.46.0 h1:NwMeyiYuVxrrHnCqrs9ZRUJu9UY1JzgW2BCr3ETNB+0= +github.com/cli/cli/v2 v2.46.0/go.mod h1:fT+63cxZjrBUnYauUq9xMBEPL913JMk/ZxHtuH57CfU= +github.com/cli/go-gh/v2 v2.9.0 h1:D3lTjEneMYl54M+WjZ+kRPrR5CEJ5BHS05isBPOV3LI= +github.com/cli/go-gh/v2 v2.9.0/go.mod h1:MeRoKzXff3ygHu7zP+NVTT+imcHW6p3tpuxHAzRM2xE= +github.com/cli/oauth v1.0.1 h1:pXnTFl/qUegXHK531Dv0LNjW4mLx626eS42gnzfXJPA= +github.com/cli/oauth v1.0.1/go.mod h1:qd/FX8ZBD6n1sVNQO3aIdRxeu5LGw9WhKnYhIIoC2A4= +github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= +github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00= +github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= +github.com/cli/shurcooL-graphql v0.0.4 h1:6MogPnQJLjKkaXPyGqPRXOI2qCsQdqNfUY1QSJu2GuY= +github.com/cli/shurcooL-graphql v0.0.4/go.mod h1:3waN4u02FiZivIV+p1y4d0Jo1jc6BViMA73C+sZo2fk= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudevents/sdk-go/observability/opencensus/v2 v2.15.2 h1:AbtPqiUDzKup5JpTZzO297/QXgL/TAdpdXQCNwLzlaM= github.com/cloudevents/sdk-go/observability/opencensus/v2 v2.15.2/go.mod h1:ZbYLE+yaEQ2j4vbRc9qzvGmg30A9LhwFt/1bSebNnbU= @@ -687,13 +713,17 @@ github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo= github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= +github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs= +github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= @@ -723,6 +753,7 @@ github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lSh github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= @@ -778,6 +809,8 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -852,8 +885,8 @@ github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-containerregistry v0.19.1 h1:yMQ62Al6/V0Z7CqIrrS1iYoA5/oQCm88DeNujc7C1KY= -github.com/google/go-containerregistry v0.19.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= +github.com/google/go-containerregistry v0.19.2 h1:TannFKE1QSajsP6hPWb5oJNgKe1IKjHukIKDUmvsV6w= +github.com/google/go-containerregistry v0.19.2/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= github.com/google/go-github/scrape v0.0.0-20240403195118-24209f034709 h1:VRG+f7JgOukeWfJbgBLjkAkvvgQE97WPJYw199WfGL8= github.com/google/go-github/scrape v0.0.0-20240403195118-24209f034709/go.mod h1:8gdN46n0rSW2jEvBD7hp2YL1IkJRewL4wAFVNA7FjLE= github.com/google/go-github/v60 v60.0.0 h1:oLG98PsLauFvvu4D/YPxq374jhSxFYdzQGNCyONLfn8= @@ -891,6 +924,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/s2a-go v0.1.0/go.mod h1:OJpEgntRZo8ugHpF9hkoLJbS5dSI20XZeXJ9JVywLlM= github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -926,6 +961,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4Zs github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsDhiblTUXHxcOPwQSCzi7xpQUN4= github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -945,6 +982,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/henvic/httpretty v0.1.3 h1:4A6vigjz6Q/+yAfTD4wqipCv+Px69C7Th/NhT0ApuU8= +github.com/henvic/httpretty v0.1.3/go.mod h1:UUEv7c2kHZ5SPQ51uS3wBpzPDibg2U3Y+IaXyHy5GBg= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -955,6 +994,10 @@ github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/itchyny/gojq v0.12.15 h1:WC1Nxbx4Ifw5U2oQWACYz32JK8G9qxNtHzrvW4KEcqI= +github.com/itchyny/gojq v0.12.15/go.mod h1:uWAHCbCIla1jiNxmeT5/B5mOjSdfkCq6p8vxWg+BM10= +github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE= +github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -1001,6 +1044,8 @@ github.com/ktrysmt/go-bitbucket v0.9.77 h1:D/xyUFLDaxI9USElXzjQr+s4hYR22vcsIl8+d github.com/ktrysmt/go-bitbucket v0.9.77/go.mod h1:7dN/AHXCCG9TW2KzXdJJy2J2uvcvZCLJR4uXYr1Gx/A= github.com/lightstep/tracecontext.go v0.0.0-20181129014701-1757c391b1ac h1:+2b6iGRJe3hvV/yVXrd41yVEjxuFHxasJqDhkIjS4gk= github.com/lightstep/tracecontext.go v0.0.0-20181129014701-1757c391b1ac/go.mod h1:Frd2bnT3w5FB5q49ENTfVlztJES+1k/7lyWX2+9gq/M= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= @@ -1020,6 +1065,9 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= @@ -1037,6 +1085,10 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -1067,8 +1119,9 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -1112,6 +1165,10 @@ github.com/rickb777/date v1.20.2 h1:CUpAaa4ksqvcRaidSgwzK7zeO2wUG5/VGy6Zlfcu/d4= github.com/rickb777/date v1.20.2/go.mod h1:PVaM/Zn0IOzjm1uj84Eh9NJ/imtQSm1SVKtOvIunaYw= github.com/rickb777/plural v1.4.1 h1:5MMLcbIaapLFmvDGRT5iPk8877hpTPt8Y9cdSKRw9sU= github.com/rickb777/plural v1.4.1/go.mod h1:kdmXUpmKBJTS0FtG/TFumd//VBWsNTD7zOw7x4umxNw= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= @@ -1123,6 +1180,10 @@ github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUz github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= +github.com/shurcooL/githubv4 v0.0.0-20230704064427-599ae7bbf278 h1:kdEGVAV4sO46DPtb8k793jiecUEhaX9ixoIBt41HEGU= +github.com/shurcooL/githubv4 v0.0.0-20230704064427-599ae7bbf278/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8= +github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0= +github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= @@ -1130,8 +1191,8 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= @@ -1142,6 +1203,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -1158,6 +1220,8 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8 github.com/stvp/go-udp-testing v0.0.0-20201019212854-469649b16807/go.mod h1:7jxmlfBCDBXRzr0eAQJ48XC1hBu1np4CS5+cHEYfwpc= github.com/tektoncd/pipeline v0.58.0 h1:+w9H2b1sMZDDirB+B7UB/cLBl0IdXx6O1tSFftMpjQw= github.com/tektoncd/pipeline v0.58.0/go.mod h1:d0+ycxWm0PP+e8pGGqp8WqLwOtp+rCgvbWCFqpdBp+Q= +github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8= +github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/xanzy/go-gitlab v0.101.0 h1:qRgvX8DNE19zRugB6rnnZMZ5ubhITSKPLNWEyc6UIPg= @@ -1174,6 +1238,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8Lba8= +github.com/zalando/go-keyring v0.2.5/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -1219,8 +1285,9 @@ golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1351,8 +1418,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1383,8 +1450,9 @@ golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= -golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1464,6 +1532,7 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1514,8 +1583,9 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1533,8 +1603,9 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1697,8 +1768,6 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -1912,8 +1981,8 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1922,6 +1991,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= +gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/pkg/cmd/tknpac/auth/keyring.go b/pkg/cmd/tknpac/auth/keyring.go new file mode 100644 index 000000000..2f736ae66 --- /dev/null +++ b/pkg/cmd/tknpac/auth/keyring.go @@ -0,0 +1,86 @@ +package auth + +import ( + "errors" + "strings" + "time" + + "github.com/zalando/go-keyring" +) + +var ErrNotFound = errors.New("secret not found in keyring") + +type TimeoutError struct { + message string +} + +func (e *TimeoutError) Error() string { + return e.message +} + +// Set secret in keyring for user. +func SetCred(service, user, secret string) error { + ch := make(chan error, 1) + go func() { + defer close(ch) + ch <- keyring.Set(keyringServiceName(service), user, secret) + }() + select { + case err := <-ch: + return err + case <-time.After(3 * time.Second): + return &TimeoutError{"timeout while trying to set secret in keyring"} + } +} + +// Get secret from keyring given service and user name. +func GetCred(service, user string) (string, error) { + ch := make(chan struct { + val string + err error + }, 1) + go func() { + defer close(ch) + val, err := keyring.Get(keyringServiceName(service), user) + ch <- struct { + val string + err error + }{val, err} + }() + select { + case res := <-ch: + if errors.Is(res.err, keyring.ErrNotFound) { + return "", ErrNotFound + } + return res.val, res.err + case <-time.After(3 * time.Second): + return "", &TimeoutError{"timeout while trying to get secret from keyring"} + } +} + +// Delete secret from keyring. +func Delete(service, user string) error { + ch := make(chan error, 1) + go func() { + defer close(ch) + ch <- keyring.Delete(service, user) + }() + select { + case err := <-ch: + return err + case <-time.After(3 * time.Second): + return &TimeoutError{"timeout while trying to delete secret from keyring"} + } +} + +func keyringServiceName(hostname string) string { + switch { + case strings.Contains(hostname, "github"): + return "gh:" + hostname + case strings.Contains(hostname, "gitlab"): + return "gl:" + hostname + case strings.Contains(hostname, "bitbucket"): + return "bb:" + hostname + } + return hostname +} diff --git a/pkg/cmd/tknpac/auth/login.go b/pkg/cmd/tknpac/auth/login.go new file mode 100644 index 000000000..670bf93db --- /dev/null +++ b/pkg/cmd/tknpac/auth/login.go @@ -0,0 +1,153 @@ +package auth + +import ( + "fmt" + "net/http" + + "github.com/AlecAivazis/survey/v2" + "github.com/cli/cli/v2/pkg/cmd/auth/shared" + "github.com/openshift-pipelines/pipelines-as-code/pkg/cli" + "github.com/openshift-pipelines/pipelines-as-code/pkg/params" + "github.com/spf13/cobra" +) + +var ( + provider string + hostname string + authMode string +) + +func loginCommand(_ *params.Run, ioStreams *cli.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "login", + Short: "login user with provider", + RunE: func(_ *cobra.Command, _ []string) error { + var authToken string + var username string + var err error + cs := ioStreams.ColorScheme() + + if provider != "github" && provider != "gitlab" && provider != "bitbucket" { + return fmt.Errorf("provide is invalid must amongst these three [github, gitlab, bitbucket]") + } + + if provider != "github" { + return fmt.Errorf("feature is in under development, at the moment only github is supported") + } + + hosts := []string{"Github.com", "Github Enterprise Server"} + authModes := []string{"Login with web browser", "Paste an authentication token"} + hostname, authMode, err = questions(hosts, authModes) + if err != nil { + return err + } + + if hostname == hosts[0] { + hostname = defaultGithubHostname + } else { + hostname, err = askForEnterpriseHostName() + if err != nil { + return err + } + } + + if authMode == authModes[0] { + authToken, username, err = RunAuthFlow(hostname, ioStreams, "", []string{}, true) + if err != nil { + return fmt.Errorf("failed to authenticate via web browser: %w", err) + } + fmt.Fprintf(ioStreams.ErrOut, "%s Authentication complete for user %s.\n", cs.SuccessIcon(), cs.GreenBold(username)) + } else { + minimumScopes := []string{"repo", "read:org"} + fmt.Fprintf(ioStreams.ErrOut, ` + Tip: you can generate a Personal Access Token here https://%s/settings/tokens + The minimum required scopes are %s. + `, hostname, scopesSentence(minimumScopes)) + + authToken, err = askForAuthToken() + if err != nil { + return err + } + + // checking github permission scopes for authToken + if err = shared.HasMinimumScopes(http.DefaultClient, hostname, authToken); err != nil { + return fmt.Errorf("error validating token: %w", err) + } + } + + err = SetCred(hostname, username, authToken) + if err != nil { + return fmt.Errorf("error saving token in keyring: %w", err) + } + return nil + }, + Annotations: map[string]string{ + "commandType": "main", + }, + } + + cmd.PersistentFlags().StringVarP(&provider, "provider", "p", "github", "Git provider possible values [github, gitlab, bitbucket]") + return cmd +} + +func askForAuthToken() (string, error) { + var authToken string + err := survey.Ask([]*survey.Question{{ + Name: "name", + Prompt: &survey.Input{Message: "Please enter you authentication token here:"}, + Validate: survey.Required, + Transform: survey.Title, + }}, &authToken) + if err != nil { + return "", err + } + + return authToken, nil +} + +func questions(hosts, authenticationMethods []string) (string, string, error) { + answers := struct { + HostName string `survey:"hostName"` + LoginMethod string `survey:"loginMethod"` + }{} + qs := []*survey.Question{ + { + Name: "hostName", + Prompt: &survey.Select{ + Message: "Which account do you want to log in to?", + Options: hosts, + Default: hosts[0], + }, + }, + { + Name: "loginMethod", + Prompt: &survey.Select{ + Message: "How would you like to authenticate?", + Options: authenticationMethods, + Default: authenticationMethods[0], + }, + }, + } + + err := survey.Ask(qs, &answers) + if err != nil { + return "", "", err + } + + return answers.HostName, answers.LoginMethod, nil +} + +func askForEnterpriseHostName() (string, error) { + var hostName string + err := survey.Ask([]*survey.Question{{ + Name: "enterpriseHostName", + Prompt: &survey.Input{Message: "Enter your GHE hostname:"}, + Validate: survey.Required, + Transform: survey.Title, + }}, &hostName) + if err != nil { + return "", err + } + + return hostName, nil +} diff --git a/pkg/cmd/tknpac/auth/login_flow.go b/pkg/cmd/tknpac/auth/login_flow.go new file mode 100644 index 000000000..0072f6740 --- /dev/null +++ b/pkg/cmd/tknpac/auth/login_flow.go @@ -0,0 +1,187 @@ +package auth + +import ( + "bufio" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/cli/cli/v2/api" + "github.com/cli/oauth" + "github.com/openshift-pipelines/pipelines-as-code/pkg/cli" + "github.com/openshift-pipelines/pipelines-as-code/pkg/cli/browser" +) + +// DefaultGithubHostname is the domain name of the default GitHub instance. +const defaultGithubHostname = "github.com" + +// DefaultGitlabHostname is the domain name of the default GitHub instance. +// const defaultGitlabHostname = "gitlab.com" + +// DefaultBitbucketHostname is the domain name of the default GitHub instance. +// const defaultBitbucketHostname = "bitbucket.org" + +// Localhost is the domain name of a local GitHub instance. +const localhost = "github.localhost" + +// TenancyHost is the domain name of a tenancy GitHub instance. +const tenancyHost = "ghe.com" + +var ( + // oauth app client ID. + oauthClientID = "Ov23linjEIlP76Xz0qgC" + // #nosec G101: Potential hardcoded credentials + oauthClientSecret = "f38d05e5dcfa672ed0dd36ad81b98e263c68bab3" +) + +func RunAuthFlow(oauthHost string, ioStreams *cli.IOStreams, notice string, additionalScopes []string, isInteractive bool) (string, string, error) { + w := ioStreams.ErrOut + cs := ioStreams.ColorScheme() + + httpClient := &http.Client{} + // debugEnabled, debugValue := utils.IsDebugEnabled() + // if debugEnabled { + // logTraffic := strings.Contains(debugValue, "api") + // httpClient.Transport = verboseLog(IO.ErrOut, logTraffic, IO.ColorEnabled())(httpClient.Transport) + // } + + scopes := []string{"repo", "read:org", "gist"} + scopes = append(scopes, additionalScopes...) + + callbackURI := "http://127.0.0.1/callback" + if IsEnterprise(oauthHost) { + // the OAuth app on Enterprise hosts is still registered with a legacy callback URL + // see https://github.com/cli/cli/pull/222, https://github.com/cli/cli/pull/650 + callbackURI = "http://localhost/" + } + + flow := &oauth.Flow{ + Host: oauth.GitHubHost(HostPrefix(oauthHost)), + ClientID: oauthClientID, + ClientSecret: oauthClientSecret, + CallbackURI: callbackURI, + Scopes: scopes, + DisplayCode: func(code, _ string) error { + fmt.Fprintf(w, "%s First copy your one-time code: %s\n", cs.Yellow("!"), cs.Bold(code)) + return nil + }, + BrowseURL: func(authURL string) error { + if u, err := url.Parse(authURL); err == nil { + if u.Scheme != "http" && u.Scheme != "https" { + return fmt.Errorf("invalid URL: %s", authURL) + } + } else { + return err + } + + if !isInteractive { + fmt.Fprintf(w, "%s to continue in your web browser: %s\n", cs.Bold("Open this URL"), authURL) + return nil + } + + fmt.Fprintf(w, "%s to open %s in your browser... ", cs.Bold("Press Enter"), oauthHost) + _ = waitForEnter(ioStreams.In) + + if err := browser.OpenWebBrowser(authURL); err != nil { + fmt.Fprintf(w, "%s Failed opening a web browser at %s\n", cs.Red("!"), authURL) + fmt.Fprintf(w, " %s\n", err) + fmt.Fprint(w, " Please try entering the URL in your browser manually\n") + } + return nil + }, + WriteSuccessHTML: func(w io.Writer) { + fmt.Fprint(w, oauthSuccessPage) + }, + HTTPClient: httpClient, + Stdin: ioStreams.In, + Stdout: w, + } + + fmt.Fprintln(w, notice) + + token, err := flow.DetectFlow() + if err != nil { + return "", "", err + } + + userLogin, err := getViewer(oauthHost, token.Token, ioStreams.ErrOut) + if err != nil { + return "", "", err + } + + return token.Token, userLogin, nil +} + +func waitForEnter(r io.Reader) error { + scanner := bufio.NewScanner(r) + scanner.Scan() + return scanner.Err() +} + +type cfg struct { + token string +} + +func (c cfg) ActiveToken(_ string) (string, string) { + return c.token, "oauth_token" +} + +func getViewer(hostname, token string, logWriter io.Writer) (string, error) { + opts := api.HTTPClientOptions{ + Config: cfg{token: token}, + Log: logWriter, + } + client, err := api.NewHTTPClient(opts) + if err != nil { + return "", err + } + + return api.CurrentLoginName(api.NewClientFromHTTP(client), hostname) +} + +// IsEnterprise reports whether a non-normalized host name looks like a GHE instance. +func IsEnterprise(h string) bool { + normalizedHostName := NormalizeHostname(h) + return normalizedHostName != defaultGithubHostname && normalizedHostName != localhost +} + +// NormalizeHostname returns the canonical host name of a GitHub instance. +func NormalizeHostname(h string) string { + hostname := strings.ToLower(h) + if strings.HasSuffix(hostname, "."+defaultGithubHostname) { + return defaultGithubHostname + } + if strings.HasSuffix(hostname, "."+localhost) { + return localhost + } + if before, found := cutSuffix(hostname, "."+tenancyHost); found { + idx := strings.LastIndex(before, ".") + return fmt.Sprintf("%s.%s", before[idx+1:], tenancyHost) + } + return hostname +} + +func HostPrefix(hostname string) string { + if strings.EqualFold(hostname, localhost) { + return fmt.Sprintf("http://%s/", hostname) + } + return fmt.Sprintf("https://%s/", hostname) +} + +// Backport strings.CutSuffix from Go 1.20. +func cutSuffix(s, suffix string) (string, bool) { + if !strings.HasSuffix(s, suffix) { + return s, false + } + return s[:len(s)-len(suffix)], true +} + +func scopesSentence(scopes []string) string { + quoted := make([]string, len(scopes)) + for i, s := range scopes { + quoted[i] = fmt.Sprintf("'%s'", s) + } + return strings.Join(quoted, ", ") +} diff --git a/pkg/cmd/tknpac/auth/root.go b/pkg/cmd/tknpac/auth/root.go new file mode 100644 index 000000000..9cb4ae517 --- /dev/null +++ b/pkg/cmd/tknpac/auth/root.go @@ -0,0 +1,23 @@ +package auth + +import ( + "github.com/openshift-pipelines/pipelines-as-code/pkg/cli" + "github.com/openshift-pipelines/pipelines-as-code/pkg/params" + "github.com/spf13/cobra" +) + +func Root(clients *params.Run, ioStreams *cli.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "auth", + Aliases: []string{}, + Short: "Authenticate user", + Long: `Authenticate users with git provider`, + SilenceUsage: true, + Annotations: map[string]string{ + "commandType": "main", + }, + } + + cmd.AddCommand(loginCommand(clients, ioStreams)) + return cmd +} diff --git a/pkg/cmd/tknpac/auth/success.go b/pkg/cmd/tknpac/auth/success.go new file mode 100644 index 000000000..291a52141 --- /dev/null +++ b/pkg/cmd/tknpac/auth/success.go @@ -0,0 +1,42 @@ +package auth + +const oauthSuccessPage = ` + + +Success: GitHub CLI + + + +
+

Successfully authenticated GitHub CLI

+

You may now close this tab and return to the terminal.

+
+ +` diff --git a/pkg/cmd/tknpac/root.go b/pkg/cmd/tknpac/root.go index 9f4defc92..bc3232e16 100644 --- a/pkg/cmd/tknpac/root.go +++ b/pkg/cmd/tknpac/root.go @@ -2,6 +2,7 @@ package tknpac import ( "github.com/openshift-pipelines/pipelines-as-code/pkg/cli" + "github.com/openshift-pipelines/pipelines-as-code/pkg/cmd/tknpac/auth" "github.com/openshift-pipelines/pipelines-as-code/pkg/cmd/tknpac/bootstrap" "github.com/openshift-pipelines/pipelines-as-code/pkg/cmd/tknpac/completion" "github.com/openshift-pipelines/pipelines-as-code/pkg/cmd/tknpac/create" @@ -44,5 +45,6 @@ func Root(clients *params.Run) *cobra.Command { cmd.AddCommand(bootstrap.Command(clients, ioStreams)) cmd.AddCommand(generate.Command(clients, ioStreams)) cmd.AddCommand(webhook.Root(clients, ioStreams)) + cmd.AddCommand(auth.Root(clients, ioStreams)) return cmd } diff --git a/vendor/github.com/MakeNowJust/heredoc/LICENSE b/vendor/github.com/MakeNowJust/heredoc/LICENSE new file mode 100644 index 000000000..6d0eb9d5d --- /dev/null +++ b/vendor/github.com/MakeNowJust/heredoc/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014-2019 TSUYUSATO Kitsune + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/MakeNowJust/heredoc/README.md b/vendor/github.com/MakeNowJust/heredoc/README.md new file mode 100644 index 000000000..e9924d297 --- /dev/null +++ b/vendor/github.com/MakeNowJust/heredoc/README.md @@ -0,0 +1,52 @@ +# heredoc + +[![Build Status](https://circleci.com/gh/MakeNowJust/heredoc.svg?style=svg)](https://circleci.com/gh/MakeNowJust/heredoc) [![GoDoc](https://godoc.org/github.com/MakeNowJusti/heredoc?status.svg)](https://godoc.org/github.com/MakeNowJust/heredoc) + +## About + +Package heredoc provides the here-document with keeping indent. + +## Install + +```console +$ go get github.com/MakeNowJust/heredoc +``` + +## Import + +```go +// usual +import "github.com/MakeNowJust/heredoc" +``` + +## Example + +```go +package main + +import ( + "fmt" + "github.com/MakeNowJust/heredoc" +) + +func main() { + fmt.Println(heredoc.Doc(` + Lorem ipsum dolor sit amet, consectetur adipisicing elit, + sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. Ut enim ad minim veniam, ... + `)) + // Output: + // Lorem ipsum dolor sit amet, consectetur adipisicing elit, + // sed do eiusmod tempor incididunt ut labore et dolore magna + // aliqua. Ut enim ad minim veniam, ... + // +} +``` + +## API Document + + - [heredoc - GoDoc](https://godoc.org/github.com/MakeNowJust/heredoc) + +## License + +This software is released under the MIT License, see LICENSE. diff --git a/vendor/github.com/MakeNowJust/heredoc/heredoc.go b/vendor/github.com/MakeNowJust/heredoc/heredoc.go new file mode 100644 index 000000000..1fc046955 --- /dev/null +++ b/vendor/github.com/MakeNowJust/heredoc/heredoc.go @@ -0,0 +1,105 @@ +// Copyright (c) 2014-2019 TSUYUSATO Kitsune +// This software is released under the MIT License. +// http://opensource.org/licenses/mit-license.php + +// Package heredoc provides creation of here-documents from raw strings. +// +// Golang supports raw-string syntax. +// +// doc := ` +// Foo +// Bar +// ` +// +// But raw-string cannot recognize indentation. Thus such content is an indented string, equivalent to +// +// "\n\tFoo\n\tBar\n" +// +// I dont't want this! +// +// However this problem is solved by package heredoc. +// +// doc := heredoc.Doc(` +// Foo +// Bar +// `) +// +// Is equivalent to +// +// "Foo\nBar\n" +package heredoc + +import ( + "fmt" + "strings" + "unicode" +) + +const maxInt = int(^uint(0) >> 1) + +// Doc returns un-indented string as here-document. +func Doc(raw string) string { + skipFirstLine := false + if len(raw) > 0 && raw[0] == '\n' { + raw = raw[1:] + } else { + skipFirstLine = true + } + + lines := strings.Split(raw, "\n") + + minIndentSize := getMinIndent(lines, skipFirstLine) + lines = removeIndentation(lines, minIndentSize, skipFirstLine) + + return strings.Join(lines, "\n") +} + +// getMinIndent calculates the minimum indentation in lines, excluding empty lines. +func getMinIndent(lines []string, skipFirstLine bool) int { + minIndentSize := maxInt + + for i, line := range lines { + if i == 0 && skipFirstLine { + continue + } + + indentSize := 0 + for _, r := range []rune(line) { + if unicode.IsSpace(r) { + indentSize += 1 + } else { + break + } + } + + if len(line) == indentSize { + if i == len(lines)-1 && indentSize < minIndentSize { + lines[i] = "" + } + } else if indentSize < minIndentSize { + minIndentSize = indentSize + } + } + return minIndentSize +} + +// removeIndentation removes n characters from the front of each line in lines. +// Skips first line if skipFirstLine is true, skips empty lines. +func removeIndentation(lines []string, n int, skipFirstLine bool) []string { + for i, line := range lines { + if i == 0 && skipFirstLine { + continue + } + + if len(lines[i]) >= n { + lines[i] = line[n:] + } + } + return lines +} + +// Docf returns unindented and formatted string as here-document. +// Formatting is done as for fmt.Printf(). +func Docf(raw string, args ...interface{}) string { + return fmt.Sprintf(Doc(raw), args...) +} diff --git a/vendor/github.com/alessio/shellescape/.gitignore b/vendor/github.com/alessio/shellescape/.gitignore new file mode 100644 index 000000000..4ba7c2d13 --- /dev/null +++ b/vendor/github.com/alessio/shellescape/.gitignore @@ -0,0 +1,28 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +.idea/ + +escargs diff --git a/vendor/github.com/alessio/shellescape/.golangci.yml b/vendor/github.com/alessio/shellescape/.golangci.yml new file mode 100644 index 000000000..836dabbba --- /dev/null +++ b/vendor/github.com/alessio/shellescape/.golangci.yml @@ -0,0 +1,59 @@ +# run: +# # timeout for analysis, e.g. 30s, 5m, default is 1m +# timeout: 5m + +linters: + disable-all: true + enable: + - bodyclose + - dogsled + - goconst + - gocritic + - gofmt + - goimports + - gosec + - gosimple + - govet + - ineffassign + - misspell + - prealloc + - exportloopref + - revive + - staticcheck + - stylecheck + - typecheck + - unconvert + - unparam + - unused + - misspell + - wsl + +issues: + exclude-rules: + - text: "Use of weak random number generator" + linters: + - gosec + - text: "comment on exported var" + linters: + - golint + - text: "don't use an underscore in package name" + linters: + - golint + - text: "ST1003:" + linters: + - stylecheck + # FIXME: Disabled until golangci-lint updates stylecheck with this fix: + # https://github.com/dominikh/go-tools/issues/389 + - text: "ST1016:" + linters: + - stylecheck + +linters-settings: + dogsled: + max-blank-identifiers: 3 + maligned: + # print struct with more effective memory layout or not, false by default + suggest-new: true + +run: + tests: false diff --git a/vendor/github.com/alessio/shellescape/.goreleaser.yml b/vendor/github.com/alessio/shellescape/.goreleaser.yml new file mode 100644 index 000000000..0915eb869 --- /dev/null +++ b/vendor/github.com/alessio/shellescape/.goreleaser.yml @@ -0,0 +1,54 @@ +# This is an example goreleaser.yaml file with some sane defaults. +# Make sure to check the documentation at http://goreleaser.com +before: + hooks: + # You may remove this if you don't use go modules. + - go mod download + # you may remove this if you don't need go generate + - go generate ./... +builds: + - env: + - CGO_ENABLED=0 + - >- + {{- if eq .Os "darwin" }} + {{- if eq .Arch "amd64"}}CC=o64-clang{{- end }} + {{- if eq .Arch "arm64"}}CC=aarch64-apple-darwin20.2-clang{{- end }} + {{- end }} + {{- if eq .Os "windows" }} + {{- if eq .Arch "amd64" }}CC=x86_64-w64-mingw32-gcc{{- end }} + {{- end }} + main: ./cmd/escargs + goos: + - linux + - windows + - darwin + - freebsd + goarch: + - amd64 + - arm64 + - arm + goarm: + - 6 + - 7 + goamd64: + - v2 + - v3 + ignore: + - goos: darwin + goarch: 386 + - goos: linux + goarch: arm + goarm: 7 + - goarm: mips64 + - gomips: hardfloat + - goamd64: v4 +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ .Tag }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' diff --git a/vendor/github.com/alessio/shellescape/AUTHORS b/vendor/github.com/alessio/shellescape/AUTHORS new file mode 100644 index 000000000..4a647a6f4 --- /dev/null +++ b/vendor/github.com/alessio/shellescape/AUTHORS @@ -0,0 +1 @@ +Alessio Treglia diff --git a/vendor/github.com/alessio/shellescape/CODE_OF_CONDUCT.md b/vendor/github.com/alessio/shellescape/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..e8eda6062 --- /dev/null +++ b/vendor/github.com/alessio/shellescape/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at alessio@debian.org. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/vendor/github.com/alessio/shellescape/LICENSE b/vendor/github.com/alessio/shellescape/LICENSE new file mode 100644 index 000000000..9f760679f --- /dev/null +++ b/vendor/github.com/alessio/shellescape/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Alessio Treglia + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/alessio/shellescape/README.md b/vendor/github.com/alessio/shellescape/README.md new file mode 100644 index 000000000..910bb253b --- /dev/null +++ b/vendor/github.com/alessio/shellescape/README.md @@ -0,0 +1,61 @@ +![Build](https://github.com/alessio/shellescape/workflows/Build/badge.svg) +[![GoDoc](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/alessio/shellescape?tab=overview) +[![sourcegraph](https://sourcegraph.com/github.com/alessio/shellescape/-/badge.svg)](https://sourcegraph.com/github.com/alessio/shellescape) +[![codecov](https://codecov.io/gh/alessio/shellescape/branch/master/graph/badge.svg)](https://codecov.io/gh/alessio/shellescape) +[![Coverage](https://gocover.io/_badge/github.com/alessio/shellescape)](https://gocover.io/github.com/alessio/shellescape) +[![Go Report Card](https://goreportcard.com/badge/github.com/alessio/shellescape)](https://goreportcard.com/report/github.com/alessio/shellescape) + +# shellescape +Escape arbitrary strings for safe use as command line arguments. +## Contents of the package + +This package provides the `shellescape.Quote()` function that returns a +shell-escaped copy of a string. This functionality could be helpful +in those cases where it is known that the output of a Go program will +be appended to/used in the context of shell programs' command line arguments. + +This work was inspired by the Python original package +[shellescape](https://pypi.python.org/pypi/shellescape). + +## Usage + +The following snippet shows a typical unsafe idiom: + +```go +package main + +import ( + "fmt" + "os" +) + +func main() { + fmt.Printf("ls -l %s\n", os.Args[1]) +} +``` +_[See in Go Playground](https://play.golang.org/p/Wj2WoUfH_d)_ + +Especially when creating pipeline of commands which might end up being +executed by a shell interpreter, it is particularly unsafe to not +escape arguments. + +`shellescape.Quote()` comes in handy and to safely escape strings: + +```go +package main + +import ( + "fmt" + "os" + + "gopkg.in/alessio/shellescape.v1" +) + +func main() { + fmt.Printf("ls -l %s\n", shellescape.Quote(os.Args[1])) +} +``` +_[See in Go Playground](https://play.golang.org/p/HJ_CXgSrmp)_ + +## The escargs utility +__escargs__ reads lines from the standard input and prints shell-escaped versions. Unlinke __xargs__, blank lines on the standard input are not discarded. diff --git a/vendor/github.com/alessio/shellescape/shellescape.go b/vendor/github.com/alessio/shellescape/shellescape.go new file mode 100644 index 000000000..dc34a556a --- /dev/null +++ b/vendor/github.com/alessio/shellescape/shellescape.go @@ -0,0 +1,66 @@ +/* +Package shellescape provides the shellescape.Quote to escape arbitrary +strings for a safe use as command line arguments in the most common +POSIX shells. + +The original Python package which this work was inspired by can be found +at https://pypi.python.org/pypi/shellescape. +*/ +package shellescape // "import gopkg.in/alessio/shellescape.v1" + +/* +The functionality provided by shellescape.Quote could be helpful +in those cases where it is known that the output of a Go program will +be appended to/used in the context of shell programs' command line arguments. +*/ + +import ( + "regexp" + "strings" + "unicode" +) + +var pattern *regexp.Regexp + +func init() { + pattern = regexp.MustCompile(`[^\w@%+=:,./-]`) +} + +// Quote returns a shell-escaped version of the string s. The returned value +// is a string that can safely be used as one token in a shell command line. +func Quote(s string) string { + if len(s) == 0 { + return "''" + } + + if pattern.MatchString(s) { + return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'" + } + + return s +} + +// QuoteCommand returns a shell-escaped version of the slice of strings. +// The returned value is a string that can safely be used as shell command arguments. +func QuoteCommand(args []string) string { + l := make([]string, len(args)) + + for i, s := range args { + l[i] = Quote(s) + } + + return strings.Join(l, " ") +} + +// StripUnsafe remove non-printable runes, e.g. control characters in +// a string that is meant for consumption by terminals that support +// control characters. +func StripUnsafe(s string) string { + return strings.Map(func(r rune) rune { + if unicode.IsPrint(r) { + return r + } + + return -1 + }, s) +} diff --git a/vendor/github.com/aymanbagabas/go-osc52/v2/LICENSE b/vendor/github.com/aymanbagabas/go-osc52/v2/LICENSE new file mode 100644 index 000000000..25cec1ed4 --- /dev/null +++ b/vendor/github.com/aymanbagabas/go-osc52/v2/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Ayman Bagabas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/aymanbagabas/go-osc52/v2/README.md b/vendor/github.com/aymanbagabas/go-osc52/v2/README.md new file mode 100644 index 000000000..4de3a22d1 --- /dev/null +++ b/vendor/github.com/aymanbagabas/go-osc52/v2/README.md @@ -0,0 +1,83 @@ + +# go-osc52 + +

+ Latest Release + GoDoc +

+ +A Go library to work with the [ANSI OSC52](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands) terminal sequence. + +## Usage + +You can use this small library to construct an ANSI OSC52 sequence suitable for +your terminal. + + +### Example + +```go +import ( + "os" + "fmt" + + "github.com/aymanbagabas/go-osc52/v2" +) + +func main() { + s := "Hello World!" + + // Copy `s` to system clipboard + osc52.New(s).WriteTo(os.Stderr) + + // Copy `s` to primary clipboard (X11) + osc52.New(s).Primary().WriteTo(os.Stderr) + + // Query the clipboard + osc52.Query().WriteTo(os.Stderr) + + // Clear system clipboard + osc52.Clear().WriteTo(os.Stderr) + + // Use the fmt.Stringer interface to copy `s` to system clipboard + fmt.Fprint(os.Stderr, osc52.New(s)) + + // Or to primary clipboard + fmt.Fprint(os.Stderr, osc52.New(s).Primary()) +} +``` + +## SSH Example + +You can use this over SSH using [gliderlabs/ssh](https://github.com/gliderlabs/ssh) for instance: + +```go +var sshSession ssh.Session +seq := osc52.New("Hello awesome!") +// Check if term is screen or tmux +pty, _, _ := s.Pty() +if pty.Term == "screen" { + seq = seq.Screen() +} else if isTmux { + seq = seq.Tmux() +} +seq.WriteTo(sshSession.Stderr()) +``` + +## Tmux + +Make sure you have `set-clipboard on` in your config, otherwise, tmux won't +allow your application to access the clipboard [^1]. + +Using the tmux option, `osc52.TmuxMode` or `osc52.New(...).Tmux()`, wraps the +OSC52 sequence in a special tmux DCS sequence and pass it to the outer +terminal. This requires `allow-passthrough on` in your config. +`allow-passthrough` is no longer enabled by default +[since tmux 3.3a](https://github.com/tmux/tmux/issues/3218#issuecomment-1153089282) [^2]. + +[^1]: See [tmux clipboard](https://github.com/tmux/tmux/wiki/Clipboard) +[^2]: [What is allow-passthrough](https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it) + +## Credits + +* [vim-oscyank](https://github.com/ojroques/vim-oscyank) this is heavily inspired by vim-oscyank. diff --git a/vendor/github.com/aymanbagabas/go-osc52/v2/osc52.go b/vendor/github.com/aymanbagabas/go-osc52/v2/osc52.go new file mode 100644 index 000000000..dc758d286 --- /dev/null +++ b/vendor/github.com/aymanbagabas/go-osc52/v2/osc52.go @@ -0,0 +1,305 @@ +// OSC52 is a terminal escape sequence that allows copying text to the clipboard. +// +// The sequence consists of the following: +// +// OSC 52 ; Pc ; Pd BEL +// +// Pc is the clipboard choice: +// +// c: clipboard +// p: primary +// q: secondary (not supported) +// s: select (not supported) +// 0-7: cut-buffers (not supported) +// +// Pd is the data to copy to the clipboard. This string should be encoded in +// base64 (RFC-4648). +// +// If Pd is "?", the terminal replies to the host with the current contents of +// the clipboard. +// +// If Pd is neither a base64 string nor "?", the terminal clears the clipboard. +// +// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +// where Ps = 52 => Manipulate Selection Data. +// +// Examples: +// +// // copy "hello world" to the system clipboard +// fmt.Fprint(os.Stderr, osc52.New("hello world")) +// +// // copy "hello world" to the primary Clipboard +// fmt.Fprint(os.Stderr, osc52.New("hello world").Primary()) +// +// // limit the size of the string to copy 10 bytes +// fmt.Fprint(os.Stderr, osc52.New("0123456789").Limit(10)) +// +// // escape the OSC52 sequence for screen using DCS sequences +// fmt.Fprint(os.Stderr, osc52.New("hello world").Screen()) +// +// // escape the OSC52 sequence for Tmux +// fmt.Fprint(os.Stderr, osc52.New("hello world").Tmux()) +// +// // query the system Clipboard +// fmt.Fprint(os.Stderr, osc52.Query()) +// +// // query the primary clipboard +// fmt.Fprint(os.Stderr, osc52.Query().Primary()) +// +// // clear the system Clipboard +// fmt.Fprint(os.Stderr, osc52.Clear()) +// +// // clear the primary Clipboard +// fmt.Fprint(os.Stderr, osc52.Clear().Primary()) +package osc52 + +import ( + "encoding/base64" + "fmt" + "io" + "strings" +) + +// Clipboard is the clipboard buffer to use. +type Clipboard rune + +const ( + // SystemClipboard is the system clipboard buffer. + SystemClipboard Clipboard = 'c' + // PrimaryClipboard is the primary clipboard buffer (X11). + PrimaryClipboard = 'p' +) + +// Mode is the mode to use for the OSC52 sequence. +type Mode uint + +const ( + // DefaultMode is the default OSC52 sequence mode. + DefaultMode Mode = iota + // ScreenMode escapes the OSC52 sequence for screen using DCS sequences. + ScreenMode + // TmuxMode escapes the OSC52 sequence for tmux. Not needed if tmux + // clipboard is set to `set-clipboard on` + TmuxMode +) + +// Operation is the OSC52 operation. +type Operation uint + +const ( + // SetOperation is the copy operation. + SetOperation Operation = iota + // QueryOperation is the query operation. + QueryOperation + // ClearOperation is the clear operation. + ClearOperation +) + +// Sequence is the OSC52 sequence. +type Sequence struct { + str string + limit int + op Operation + mode Mode + clipboard Clipboard +} + +var _ fmt.Stringer = Sequence{} + +var _ io.WriterTo = Sequence{} + +// String returns the OSC52 sequence. +func (s Sequence) String() string { + var seq strings.Builder + // mode escape sequences start + seq.WriteString(s.seqStart()) + // actual OSC52 sequence start + seq.WriteString(fmt.Sprintf("\x1b]52;%c;", s.clipboard)) + switch s.op { + case SetOperation: + str := s.str + if s.limit > 0 && len(str) > s.limit { + return "" + } + b64 := base64.StdEncoding.EncodeToString([]byte(str)) + switch s.mode { + case ScreenMode: + // Screen doesn't support OSC52 but will pass the contents of a DCS + // sequence to the outer terminal unchanged. + // + // Here, we split the encoded string into 76 bytes chunks and then + // join the chunks with sequences. Finally, + // wrap the whole thing in + // . + // s := strings.SplitN(b64, "", 76) + s := make([]string, 0, len(b64)/76+1) + for i := 0; i < len(b64); i += 76 { + end := i + 76 + if end > len(b64) { + end = len(b64) + } + s = append(s, b64[i:end]) + } + seq.WriteString(strings.Join(s, "\x1b\\\x1bP")) + default: + seq.WriteString(b64) + } + case QueryOperation: + // OSC52 queries the clipboard using "?" + seq.WriteString("?") + case ClearOperation: + // OSC52 clears the clipboard if the data is neither a base64 string nor "?" + // we're using "!" as a default + seq.WriteString("!") + } + // actual OSC52 sequence end + seq.WriteString("\x07") + // mode escape end + seq.WriteString(s.seqEnd()) + return seq.String() +} + +// WriteTo writes the OSC52 sequence to the writer. +func (s Sequence) WriteTo(out io.Writer) (int64, error) { + n, err := out.Write([]byte(s.String())) + return int64(n), err +} + +// Mode sets the mode for the OSC52 sequence. +func (s Sequence) Mode(m Mode) Sequence { + s.mode = m + return s +} + +// Tmux sets the mode to TmuxMode. +// Used to escape the OSC52 sequence for `tmux`. +// +// Note: this is not needed if tmux clipboard is set to `set-clipboard on`. If +// TmuxMode is used, tmux must have `allow-passthrough on` set. +// +// This is a syntactic sugar for s.Mode(TmuxMode). +func (s Sequence) Tmux() Sequence { + return s.Mode(TmuxMode) +} + +// Screen sets the mode to ScreenMode. +// Used to escape the OSC52 sequence for `screen`. +// +// This is a syntactic sugar for s.Mode(ScreenMode). +func (s Sequence) Screen() Sequence { + return s.Mode(ScreenMode) +} + +// Clipboard sets the clipboard buffer for the OSC52 sequence. +func (s Sequence) Clipboard(c Clipboard) Sequence { + s.clipboard = c + return s +} + +// Primary sets the clipboard buffer to PrimaryClipboard. +// This is the X11 primary clipboard. +// +// This is a syntactic sugar for s.Clipboard(PrimaryClipboard). +func (s Sequence) Primary() Sequence { + return s.Clipboard(PrimaryClipboard) +} + +// Limit sets the limit for the OSC52 sequence. +// The default limit is 0 (no limit). +// +// Strings longer than the limit get ignored. Settting the limit to 0 or a +// negative value disables the limit. Each terminal defines its own escapse +// sequence limit. +func (s Sequence) Limit(l int) Sequence { + if l < 0 { + s.limit = 0 + } else { + s.limit = l + } + return s +} + +// Operation sets the operation for the OSC52 sequence. +// The default operation is SetOperation. +func (s Sequence) Operation(o Operation) Sequence { + s.op = o + return s +} + +// Clear sets the operation to ClearOperation. +// This clears the clipboard. +// +// This is a syntactic sugar for s.Operation(ClearOperation). +func (s Sequence) Clear() Sequence { + return s.Operation(ClearOperation) +} + +// Query sets the operation to QueryOperation. +// This queries the clipboard contents. +// +// This is a syntactic sugar for s.Operation(QueryOperation). +func (s Sequence) Query() Sequence { + return s.Operation(QueryOperation) +} + +// SetString sets the string for the OSC52 sequence. Strings are joined with a +// space character. +func (s Sequence) SetString(strs ...string) Sequence { + s.str = strings.Join(strs, " ") + return s +} + +// New creates a new OSC52 sequence with the given string(s). Strings are +// joined with a space character. +func New(strs ...string) Sequence { + s := Sequence{ + str: strings.Join(strs, " "), + limit: 0, + mode: DefaultMode, + clipboard: SystemClipboard, + op: SetOperation, + } + return s +} + +// Query creates a new OSC52 sequence with the QueryOperation. +// This returns a new OSC52 sequence to query the clipboard contents. +// +// This is a syntactic sugar for New().Query(). +func Query() Sequence { + return New().Query() +} + +// Clear creates a new OSC52 sequence with the ClearOperation. +// This returns a new OSC52 sequence to clear the clipboard. +// +// This is a syntactic sugar for New().Clear(). +func Clear() Sequence { + return New().Clear() +} + +func (s Sequence) seqStart() string { + switch s.mode { + case TmuxMode: + // Write the start of a tmux escape sequence. + return "\x1bPtmux;\x1b" + case ScreenMode: + // Write the start of a DCS sequence. + return "\x1bP" + default: + return "" + } +} + +func (s Sequence) seqEnd() string { + switch s.mode { + case TmuxMode: + // Terminate the tmux escape sequence. + return "\x1b\\" + case ScreenMode: + // Write the end of a DCS sequence. + return "\x1b\x5c" + default: + return "" + } +} diff --git a/vendor/github.com/briandowns/spinner/.gitignore b/vendor/github.com/briandowns/spinner/.gitignore new file mode 100644 index 000000000..21ec6b71b --- /dev/null +++ b/vendor/github.com/briandowns/spinner/.gitignore @@ -0,0 +1,29 @@ +# Created by .gitignore support plugin (hsz.mobi) +### Go template +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +.idea +*.iml diff --git a/vendor/github.com/briandowns/spinner/.travis.yml b/vendor/github.com/briandowns/spinner/.travis.yml new file mode 100644 index 000000000..74d205aec --- /dev/null +++ b/vendor/github.com/briandowns/spinner/.travis.yml @@ -0,0 +1,18 @@ +arch: + - amd64 + - ppc64le +language: go +go: + - 1.16 + - 1.17.5 +env: + - GOARCH: amd64 + - GOARCH: 386 +script: + - go test -v +notifications: + email: + recipients: + - brian.downs@gmail.com + on_success: change + on_failure: always diff --git a/vendor/github.com/briandowns/spinner/LICENSE b/vendor/github.com/briandowns/spinner/LICENSE new file mode 100644 index 000000000..dd5b3a58a --- /dev/null +++ b/vendor/github.com/briandowns/spinner/LICENSE @@ -0,0 +1,174 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/vendor/github.com/briandowns/spinner/Makefile b/vendor/github.com/briandowns/spinner/Makefile new file mode 100644 index 000000000..3cfdeb23c --- /dev/null +++ b/vendor/github.com/briandowns/spinner/Makefile @@ -0,0 +1,20 @@ +GO = go + +.PHONY: deps +deps: go.mod + +go.mod: + go mod init + go mod tidy + +.PHONY: test +test: + $(GO) test -v -cover ./... + +.PHONY: check +check: + if [ -d vendor ]; then cp -r vendor/* ${GOPATH}/src/; fi + +.PHONY: clean +clean: + $(GO) clean diff --git a/vendor/github.com/briandowns/spinner/NOTICE.txt b/vendor/github.com/briandowns/spinner/NOTICE.txt new file mode 100644 index 000000000..95e2a248b --- /dev/null +++ b/vendor/github.com/briandowns/spinner/NOTICE.txt @@ -0,0 +1,4 @@ +Spinner +Copyright (c) 2022 Brian J. Downs +This product is licensed to you under the Apache 2.0 license (the "License"). You may not use this product except in compliance with the Apache 2.0 License. +This product may include a number of subcomponents with separate copyright notices and license terms. Your use of these subcomponents is subject to the terms and conditions of the subcomponent's license, as noted in the LICENSE file. diff --git a/vendor/github.com/briandowns/spinner/README.md b/vendor/github.com/briandowns/spinner/README.md new file mode 100644 index 000000000..20b315fbe --- /dev/null +++ b/vendor/github.com/briandowns/spinner/README.md @@ -0,0 +1,285 @@ +# Spinner + +[![GoDoc](https://godoc.org/github.com/briandowns/spinner?status.svg)](https://godoc.org/github.com/briandowns/spinner) [![Build Status](https://travis-ci.org/briandowns/spinner.svg?branch=master)](https://travis-ci.org/briandowns/spinner) + +spinner is a simple package to add a spinner / progress indicator to any terminal application. Examples can be found below as well as full examples in the examples directory. + +For more detail about the library and its features, reference your local godoc once installed. + +Contributions welcome! + +## Installation + +```bash +go get github.com/briandowns/spinner +``` + +## Available Character Sets + +90 Character Sets. Some examples below: + +(Numbered by their slice index) + +| index | character set | sample gif | +| ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | +| 0 | ```←↖↑↗→↘↓↙``` | ![Sample Gif](gifs/0.gif) | +| 1 | ```▁▃▄▅▆▇█▇▆▅▄▃▁``` | ![Sample Gif](gifs/1.gif) | +| 2 | ```▖▘▝▗``` | ![Sample Gif](gifs/2.gif) | +| 3 | ```┤┘┴└├┌┬┐``` | ![Sample Gif](gifs/3.gif) | +| 4 | ```◢◣◤◥``` | ![Sample Gif](gifs/4.gif) | +| 5 | ```◰◳◲◱``` | ![Sample Gif](gifs/5.gif) | +| 6 | ```◴◷◶◵``` | ![Sample Gif](gifs/6.gif) | +| 7 | ```◐◓◑◒``` | ![Sample Gif](gifs/7.gif) | +| 8 | ```.oO@*``` | ![Sample Gif](gifs/8.gif) | +| 9 | ```\|/-\``` | ![Sample Gif](gifs/9.gif) | +| 10 | ```◡◡⊙⊙◠◠``` | ![Sample Gif](gifs/10.gif) | +| 11 | ```⣾⣽⣻⢿⡿⣟⣯⣷``` | ![Sample Gif](gifs/11.gif) | +| 12 | ```>))'> >))'> >))'> >))'> >))'> <'((< <'((< <'((<``` | ![Sample Gif](gifs/12.gif) | +| 13 | ```⠁⠂⠄⡀⢀⠠⠐⠈``` | ![Sample Gif](gifs/13.gif) | +| 14 | ```⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏``` | ![Sample Gif](gifs/14.gif) | +| 15 | ```abcdefghijklmnopqrstuvwxyz``` | ![Sample Gif](gifs/15.gif) | +| 16 | ```▉▊▋▌▍▎▏▎▍▌▋▊▉``` | ![Sample Gif](gifs/16.gif) | +| 17 | ```■□▪▫``` | ![Sample Gif](gifs/17.gif) | +| 18 | ```←↑→↓``` | ![Sample Gif](gifs/18.gif) | +| 19 | ```╫╪``` | ![Sample Gif](gifs/19.gif) | +| 20 | ```⇐⇖⇑⇗⇒⇘⇓⇙``` | ![Sample Gif](gifs/20.gif) | +| 21 | ```⠁⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈⠈``` | ![Sample Gif](gifs/21.gif) | +| 22 | ```⠈⠉⠋⠓⠒⠐⠐⠒⠖⠦⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈``` | ![Sample Gif](gifs/22.gif) | +| 23 | ```⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠴⠲⠒⠂⠂⠒⠚⠙⠉⠁``` | ![Sample Gif](gifs/23.gif) | +| 24 | ```⠋⠙⠚⠒⠂⠂⠒⠲⠴⠦⠖⠒⠐⠐⠒⠓⠋``` | ![Sample Gif](gifs/24.gif) | +| 25 | ```ヲァィゥェォャュョッアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン``` | ![Sample Gif](gifs/25.gif) | +| 26 | ```. .. ...``` | ![Sample Gif](gifs/26.gif) | +| 27 | ```▁▂▃▄▅▆▇█▉▊▋▌▍▎▏▏▎▍▌▋▊▉█▇▆▅▄▃▂▁``` | ![Sample Gif](gifs/27.gif) | +| 28 | ```.oO°Oo.``` | ![Sample Gif](gifs/28.gif) | +| 29 | ```+x``` | ![Sample Gif](gifs/29.gif) | +| 30 | ```v<^>``` | ![Sample Gif](gifs/30.gif) | +| 31 | ```>>---> >>---> >>---> >>---> >>---> <---<< <---<< <---<< <---<< <---<<``` | ![Sample Gif](gifs/31.gif) | +| 32 | ```\| \|\| \|\|\| \|\|\|\| \|\|\|\|\| \|\|\|\|\|\| \|\|\|\|\| \|\|\|\| \|\|\| \|\| \|``` | ![Sample Gif](gifs/32.gif) | +| 33 | ```[] [=] [==] [===] [====] [=====] [======] [=======] [========] [=========] [==========]``` | ![Sample Gif](gifs/33.gif) | +| 34 | ```(*---------) (-*--------) (--*-------) (---*------) (----*-----) (-----*----) (------*---) (-------*--) (--------*-) (---------*)``` | ![Sample Gif](gifs/34.gif) | +| 35 | ```█▒▒▒▒▒▒▒▒▒ ███▒▒▒▒▒▒▒ █████▒▒▒▒▒ ███████▒▒▒ ██████████``` | ![Sample Gif](gifs/35.gif) | +| 36 | ```[ ] [=> ] [===> ] [=====> ] [======> ] [========> ] [==========> ] [============> ] [==============> ] [================> ] [==================> ] [===================>]``` | ![Sample Gif](gifs/36.gif) | +| 37 | ```🕐 🕑 🕒 🕓 🕔 🕕 🕖 🕗 🕘 🕙 🕚 🕛``` | ![Sample Gif](gifs/37.gif) | +| 38 | ```🕐 🕜 🕑 🕝 🕒 🕞 🕓 🕟 🕔 🕠 🕕 🕡 🕖 🕢 🕗 🕣 🕘 🕤 🕙 🕥 🕚 🕦 🕛 🕧``` | ![Sample Gif](gifs/38.gif) | +| 39 | ```🌍 🌎 🌏``` | ![Sample Gif](gifs/39.gif) | +| 40 | ```◜ ◝ ◞ ◟``` | ![Sample Gif](gifs/40.gif) | +| 41 | ```⬒ ⬔ ⬓ ⬕``` | ![Sample Gif](gifs/41.gif) | +| 42 | ```⬖ ⬘ ⬗ ⬙``` | ![Sample Gif](gifs/42.gif) | +| 43 | ```[>>> >] []>>>> [] [] >>>> [] [] >>>> [] [] >>>> [] [] >>>>[] [>> >>]``` | ![Sample Gif](gifs/43.gif) | + +## Features + +* Start +* Stop +* Restart +* Reverse direction +* Update the spinner character set +* Update the spinner speed +* Prefix or append text +* Change spinner color, background, and text attributes such as bold / italics +* Get spinner status +* Chain, pipe, redirect output +* Output final string on spinner/indicator completion + +## Examples + +```Go +package main + +import ( + "github.com/briandowns/spinner" + "time" +) + +func main() { + s := spinner.New(spinner.CharSets[9], 100*time.Millisecond) // Build our new spinner + s.Start() // Start the spinner + time.Sleep(4 * time.Second) // Run for some time to simulate work + s.Stop() +} +``` + +## Update the character set and restart the spinner + +```Go +s.UpdateCharSet(spinner.CharSets[1]) // Update spinner to use a different character set +s.Restart() // Restart the spinner +time.Sleep(4 * time.Second) +s.Stop() +``` + +## Update spin speed and restart the spinner + +```Go +s.UpdateSpeed(200 * time.Millisecond) // Update the speed the spinner spins at +s.Restart() +time.Sleep(4 * time.Second) +s.Stop() +``` + +## Reverse the direction of the spinner + +```Go +s.Reverse() // Reverse the direction the spinner is spinning +s.Restart() +time.Sleep(4 * time.Second) +s.Stop() +``` + +## Provide your own spinner + +(or send me an issue or pull request to add to the project) + +```Go +someSet := []string{"+", "-"} +s := spinner.New(someSet, 100*time.Millisecond) +``` + +## Prefix or append text to the spinner + +```Go +s.Prefix = "prefixed text: " // Prefix text before the spinner +s.Suffix = " :appended text" // Append text after the spinner +``` + +## Set or change the color of the spinner. Default color is white. The spinner will need to be restarted to pick up the change. + +```Go +s.Color("red") // Set the spinner color to red +``` + +You can specify both the background and foreground color, as well as additional attributes such as `bold` or `underline`. + +```Go +s.Color("red", "bold") // Set the spinner color to a bold red +``` + +To set the background to black, the foreground to a bold red: + +```Go +s.Color("bgBlack", "bold", "fgRed") +``` + +Below is the full color and attribute list: + +```Go +// default colors +red +black +green +yellow +blue +magenta +cyan +white + +// attributes +reset +bold +faint +italic +underline +blinkslow +blinkrapid +reversevideo +concealed +crossedout + +// foreground text +fgBlack +fgRed +fgGreen +fgYellow +fgBlue +fgMagenta +fgCyan +fgWhite + +// foreground Hi-Intensity text +fgHiBlack +fgHiRed +fgHiGreen +fgHiYellow +fgHiBlue +fgHiMagenta +fgHiCyan +fgHiWhite + +// background text +bgBlack +bgRed +bgGreen +bgYellow +bgBlue +bgMagenta +bgCyan +bgWhite + +// background Hi-Intensity text +bgHiBlack +bgHiRed +bgHiGreen +bgHiYellow +bgHiBlue +bgHiMagenta +bgHiCyan +bgHiWhite +``` + +## Generate a sequence of numbers + +```Go +setOfDigits := spinner.GenerateNumberSequence(25) // Generate a 25 digit string of numbers +s := spinner.New(setOfDigits, 100*time.Millisecond) +``` + +## Get spinner status + +```Go +fmt.Println(s.Active()) +``` + +## Unix pipe and redirect + +Feature suggested and write up by [dekz](https://github.com/dekz) + +Setting the Spinner Writer to Stderr helps show progress to the user, with the enhancement to chain, pipe or redirect the output. + +This is the preferred method of setting a Writer at this time. + +```go +s := spinner.New(spinner.CharSets[11], 100*time.Millisecond, spinner.WithWriter(os.Stderr)) +s.Suffix = " Encrypting data..." +s.Start() +// Encrypt the data into ciphertext +fmt.Println(os.Stdout, ciphertext) +``` + +```sh +> myprog encrypt "Secret text" > encrypted.txt +⣯ Encrypting data... +``` + +```sh +> cat encrypted.txt +1243hjkbas23i9ah27sj39jghv237n2oa93hg83 +``` + +## Final String Output + +Add additional output when the spinner/indicator has completed. The "final" output string can be multi-lined and will be written to wherever the `io.Writer` has been configured for. + +```Go +s := spinner.New(spinner.CharSets[9], 100*time.Millisecond) +s.FinalMSG = "Complete!\nNew line!\nAnother one!\n" +s.Start() +time.Sleep(4 * time.Second) +s.Stop() +``` + +Output +```sh +Complete! +New line! +Another one! +``` diff --git a/vendor/github.com/briandowns/spinner/character_sets.go b/vendor/github.com/briandowns/spinner/character_sets.go new file mode 100644 index 000000000..df41a0f2c --- /dev/null +++ b/vendor/github.com/briandowns/spinner/character_sets.go @@ -0,0 +1,121 @@ +// Copyright (c) 2022 Brian J. Downs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package spinner + +const ( + clockOneOClock = '\U0001F550' + clockOneThirty = '\U0001F55C' +) + +// CharSets contains the available character sets +var CharSets = map[int][]string{ + 0: {"←", "↖", "↑", "↗", "→", "↘", "↓", "↙"}, + 1: {"▁", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃", "▁"}, + 2: {"▖", "▘", "▝", "▗"}, + 3: {"┤", "┘", "┴", "└", "├", "┌", "┬", "┐"}, + 4: {"◢", "◣", "◤", "◥"}, + 5: {"◰", "◳", "◲", "◱"}, + 6: {"◴", "◷", "◶", "◵"}, + 7: {"◐", "◓", "◑", "◒"}, + 8: {".", "o", "O", "@", "*"}, + 9: {"|", "/", "-", "\\"}, + 10: {"◡◡", "⊙⊙", "◠◠"}, + 11: {"⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"}, + 12: {">))'>", " >))'>", " >))'>", " >))'>", " >))'>", " <'((<", " <'((<", " <'((<"}, + 13: {"⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"}, + 14: {"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}, + 15: {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"}, + 16: {"▉", "▊", "▋", "▌", "▍", "▎", "▏", "▎", "▍", "▌", "▋", "▊", "▉"}, + 17: {"■", "□", "▪", "▫"}, + + 18: {"←", "↑", "→", "↓"}, + 19: {"╫", "╪"}, + 20: {"⇐", "⇖", "⇑", "⇗", "⇒", "⇘", "⇓", "⇙"}, + 21: {"⠁", "⠁", "⠉", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠤", "⠄", "⠄", "⠤", "⠠", "⠠", "⠤", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋", "⠉", "⠈", "⠈"}, + 22: {"⠈", "⠉", "⠋", "⠓", "⠒", "⠐", "⠐", "⠒", "⠖", "⠦", "⠤", "⠠", "⠠", "⠤", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋", "⠉", "⠈"}, + 23: {"⠁", "⠉", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠤", "⠄", "⠄", "⠤", "⠴", "⠲", "⠒", "⠂", "⠂", "⠒", "⠚", "⠙", "⠉", "⠁"}, + 24: {"⠋", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋"}, + 25: {"ヲ", "ァ", "ィ", "ゥ", "ェ", "ォ", "ャ", "ュ", "ョ", "ッ", "ア", "イ", "ウ", "エ", "オ", "カ", "キ", "ク", "ケ", "コ", "サ", "シ", "ス", "セ", "ソ", "タ", "チ", "ツ", "テ", "ト", "ナ", "ニ", "ヌ", "ネ", "ノ", "ハ", "ヒ", "フ", "ヘ", "ホ", "マ", "ミ", "ム", "メ", "モ", "ヤ", "ユ", "ヨ", "ラ", "リ", "ル", "レ", "ロ", "ワ", "ン"}, + 26: {".", "..", "..."}, + 27: {"▁", "▂", "▃", "▄", "▅", "▆", "▇", "█", "▉", "▊", "▋", "▌", "▍", "▎", "▏", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█", "▇", "▆", "▅", "▄", "▃", "▂", "▁"}, + 28: {".", "o", "O", "°", "O", "o", "."}, + 29: {"+", "x"}, + 30: {"v", "<", "^", ">"}, + 31: {">>--->", " >>--->", " >>--->", " >>--->", " >>--->", " <---<<", " <---<<", " <---<<", " <---<<", "<---<<"}, + 32: {"|", "||", "|||", "||||", "|||||", "|||||||", "||||||||", "|||||||", "||||||", "|||||", "||||", "|||", "||", "|"}, + 33: {"[ ]", "[= ]", "[== ]", "[=== ]", "[==== ]", "[===== ]", "[====== ]", "[======= ]", "[======== ]", "[========= ]", "[==========]"}, + 34: {"(*---------)", "(-*--------)", "(--*-------)", "(---*------)", "(----*-----)", "(-----*----)", "(------*---)", "(-------*--)", "(--------*-)", "(---------*)"}, + 35: {"█▒▒▒▒▒▒▒▒▒", "███▒▒▒▒▒▒▒", "█████▒▒▒▒▒", "███████▒▒▒", "██████████"}, + 36: {"[ ]", "[=> ]", "[===> ]", "[=====> ]", "[======> ]", "[========> ]", "[==========> ]", "[============> ]", "[==============> ]", "[================> ]", "[==================> ]", "[===================>]"}, + 39: {"🌍", "🌎", "🌏"}, + 40: {"◜", "◝", "◞", "◟"}, + 41: {"⬒", "⬔", "⬓", "⬕"}, + 42: {"⬖", "⬘", "⬗", "⬙"}, + 43: {"[>>> >]", "[]>>>> []", "[] >>>> []", "[] >>>> []", "[] >>>> []", "[] >>>>[]", "[>> >>]"}, + 44: {"♠", "♣", "♥", "♦"}, + 45: {"➞", "➟", "➠", "➡", "➠", "➟"}, + 46: {" | ", ` \ `, "_ ", ` \ `, " | ", " / ", " _", " / "}, + 47: {" . . . .", ". . . .", ". . . .", ". . . .", ". . . . ", ". . . . ."}, + 48: {" | ", " / ", " _ ", ` \ `, " | ", ` \ `, " _ ", " / "}, + 49: {"⎺", "⎻", "⎼", "⎽", "⎼", "⎻"}, + 50: {"▹▹▹▹▹", "▸▹▹▹▹", "▹▸▹▹▹", "▹▹▸▹▹", "▹▹▹▸▹", "▹▹▹▹▸"}, + 51: {"[ ]", "[ =]", "[ ==]", "[ ===]", "[====]", "[=== ]", "[== ]", "[= ]"}, + 52: {"( ● )", "( ● )", "( ● )", "( ● )", "( ●)", "( ● )", "( ● )", "( ● )", "( ● )"}, + 53: {"✶", "✸", "✹", "✺", "✹", "✷"}, + 54: {"▐|\\____________▌", "▐_|\\___________▌", "▐__|\\__________▌", "▐___|\\_________▌", "▐____|\\________▌", "▐_____|\\_______▌", "▐______|\\______▌", "▐_______|\\_____▌", "▐________|\\____▌", "▐_________|\\___▌", "▐__________|\\__▌", "▐___________|\\_▌", "▐____________|\\▌", "▐____________/|▌", "▐___________/|_▌", "▐__________/|__▌", "▐_________/|___▌", "▐________/|____▌", "▐_______/|_____▌", "▐______/|______▌", "▐_____/|_______▌", "▐____/|________▌", "▐___/|_________▌", "▐__/|__________▌", "▐_/|___________▌", "▐/|____________▌"}, + 55: {"▐⠂ ▌", "▐⠈ ▌", "▐ ⠂ ▌", "▐ ⠠ ▌", "▐ ⡀ ▌", "▐ ⠠ ▌", "▐ ⠂ ▌", "▐ ⠈ ▌", "▐ ⠂ ▌", "▐ ⠠ ▌", "▐ ⡀ ▌", "▐ ⠠ ▌", "▐ ⠂ ▌", "▐ ⠈ ▌", "▐ ⠂▌", "▐ ⠠▌", "▐ ⡀▌", "▐ ⠠ ▌", "▐ ⠂ ▌", "▐ ⠈ ▌", "▐ ⠂ ▌", "▐ ⠠ ▌", "▐ ⡀ ▌", "▐ ⠠ ▌", "▐ ⠂ ▌", "▐ ⠈ ▌", "▐ ⠂ ▌", "▐ ⠠ ▌", "▐ ⡀ ▌", "▐⠠ ▌"}, + 56: {"¿", "?"}, + 57: {"⢹", "⢺", "⢼", "⣸", "⣇", "⡧", "⡗", "⡏"}, + 58: {"⢄", "⢂", "⢁", "⡁", "⡈", "⡐", "⡠"}, + 59: {". ", ".. ", "...", " ..", " .", " "}, + 60: {".", "o", "O", "°", "O", "o", "."}, + 61: {"▓", "▒", "░"}, + 62: {"▌", "▀", "▐", "▄"}, + 63: {"⊶", "⊷"}, + 64: {"▪", "▫"}, + 65: {"□", "■"}, + 66: {"▮", "▯"}, + 67: {"-", "=", "≡"}, + 68: {"d", "q", "p", "b"}, + 69: {"∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"}, + 70: {"🌑 ", "🌒 ", "🌓 ", "🌔 ", "🌕 ", "🌖 ", "🌗 ", "🌘 "}, + 71: {"☗", "☖"}, + 72: {"⧇", "⧆"}, + 73: {"◉", "◎"}, + 74: {"㊂", "㊀", "㊁"}, + 75: {"⦾", "⦿"}, + 76: {"ဝ", "၀"}, + 77: {"▌", "▀", "▐▄"}, + 78: {"⠈⠁", "⠈⠑", "⠈⠱", "⠈⡱", "⢀⡱", "⢄⡱", "⢄⡱", "⢆⡱", "⢎⡱", "⢎⡰", "⢎⡠", "⢎⡀", "⢎⠁", "⠎⠁", "⠊⠁"}, + 79: {"________", "-_______", "_-______", "__-_____", "___-____", "____-___", "_____-__", "______-_", "_______-", "________", "_______-", "______-_", "_____-__", "____-___", "___-____", "__-_____", "_-______", "-_______", "________"}, + 80: {"|_______", "_/______", "__-_____", "___\\____", "____|___", "_____/__", "______-_", "_______\\", "_______|", "______\\_", "_____-__", "____/___", "___|____", "__\\_____", "_-______"}, + 81: {"□", "◱", "◧", "▣", "■"}, + 82: {"□", "◱", "▨", "▩", "■"}, + 83: {"░", "▒", "▓", "█"}, + 84: {"░", "█"}, + 85: {"⚪", "⚫"}, + 86: {"◯", "⬤"}, + 87: {"▱", "▰"}, + 88: {"➊", "➋", "➌", "➍", "➎", "➏", "➐", "➑", "➒", "➓"}, + 89: {"½", "⅓", "⅔", "¼", "¾", "⅛", "⅜", "⅝", "⅞"}, + 90: {"↞", "↟", "↠", "↡"}, +} + +func init() { + for i := rune(0); i < 12; i++ { + CharSets[37] = append(CharSets[37], string([]rune{clockOneOClock + i})) + CharSets[38] = append(CharSets[38], string([]rune{clockOneOClock + i}), string([]rune{clockOneThirty + i})) + } +} diff --git a/vendor/github.com/briandowns/spinner/spinner.go b/vendor/github.com/briandowns/spinner/spinner.go new file mode 100644 index 000000000..f6bb029f8 --- /dev/null +++ b/vendor/github.com/briandowns/spinner/spinner.go @@ -0,0 +1,447 @@ +// Copyright (c) 2021 Brian J. Downs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package spinner is a simple package to add a spinner / progress indicator to any terminal application. +package spinner + +import ( + "errors" + "fmt" + "io" + "os" + "runtime" + "strconv" + "strings" + "sync" + "time" + "unicode/utf8" + + "github.com/fatih/color" + "github.com/mattn/go-isatty" +) + +// errInvalidColor is returned when attempting to set an invalid color +var errInvalidColor = errors.New("invalid color") + +// validColors holds an array of the only colors allowed +var validColors = map[string]bool{ + // default colors for backwards compatibility + "black": true, + "red": true, + "green": true, + "yellow": true, + "blue": true, + "magenta": true, + "cyan": true, + "white": true, + + // attributes + "reset": true, + "bold": true, + "faint": true, + "italic": true, + "underline": true, + "blinkslow": true, + "blinkrapid": true, + "reversevideo": true, + "concealed": true, + "crossedout": true, + + // foreground text + "fgBlack": true, + "fgRed": true, + "fgGreen": true, + "fgYellow": true, + "fgBlue": true, + "fgMagenta": true, + "fgCyan": true, + "fgWhite": true, + + // foreground Hi-Intensity text + "fgHiBlack": true, + "fgHiRed": true, + "fgHiGreen": true, + "fgHiYellow": true, + "fgHiBlue": true, + "fgHiMagenta": true, + "fgHiCyan": true, + "fgHiWhite": true, + + // background text + "bgBlack": true, + "bgRed": true, + "bgGreen": true, + "bgYellow": true, + "bgBlue": true, + "bgMagenta": true, + "bgCyan": true, + "bgWhite": true, + + // background Hi-Intensity text + "bgHiBlack": true, + "bgHiRed": true, + "bgHiGreen": true, + "bgHiYellow": true, + "bgHiBlue": true, + "bgHiMagenta": true, + "bgHiCyan": true, + "bgHiWhite": true, +} + +// returns true if the OS is windows and the WT_SESSION env variable is set. +var isWindowsTerminalOnWindows = len(os.Getenv("WT_SESSION")) > 0 && runtime.GOOS == "windows" + +// returns a valid color's foreground text color attribute +var colorAttributeMap = map[string]color.Attribute{ + // default colors for backwards compatibility + "black": color.FgBlack, + "red": color.FgRed, + "green": color.FgGreen, + "yellow": color.FgYellow, + "blue": color.FgBlue, + "magenta": color.FgMagenta, + "cyan": color.FgCyan, + "white": color.FgWhite, + + // attributes + "reset": color.Reset, + "bold": color.Bold, + "faint": color.Faint, + "italic": color.Italic, + "underline": color.Underline, + "blinkslow": color.BlinkSlow, + "blinkrapid": color.BlinkRapid, + "reversevideo": color.ReverseVideo, + "concealed": color.Concealed, + "crossedout": color.CrossedOut, + + // foreground text colors + "fgBlack": color.FgBlack, + "fgRed": color.FgRed, + "fgGreen": color.FgGreen, + "fgYellow": color.FgYellow, + "fgBlue": color.FgBlue, + "fgMagenta": color.FgMagenta, + "fgCyan": color.FgCyan, + "fgWhite": color.FgWhite, + + // foreground Hi-Intensity text colors + "fgHiBlack": color.FgHiBlack, + "fgHiRed": color.FgHiRed, + "fgHiGreen": color.FgHiGreen, + "fgHiYellow": color.FgHiYellow, + "fgHiBlue": color.FgHiBlue, + "fgHiMagenta": color.FgHiMagenta, + "fgHiCyan": color.FgHiCyan, + "fgHiWhite": color.FgHiWhite, + + // background text colors + "bgBlack": color.BgBlack, + "bgRed": color.BgRed, + "bgGreen": color.BgGreen, + "bgYellow": color.BgYellow, + "bgBlue": color.BgBlue, + "bgMagenta": color.BgMagenta, + "bgCyan": color.BgCyan, + "bgWhite": color.BgWhite, + + // background Hi-Intensity text colors + "bgHiBlack": color.BgHiBlack, + "bgHiRed": color.BgHiRed, + "bgHiGreen": color.BgHiGreen, + "bgHiYellow": color.BgHiYellow, + "bgHiBlue": color.BgHiBlue, + "bgHiMagenta": color.BgHiMagenta, + "bgHiCyan": color.BgHiCyan, + "bgHiWhite": color.BgHiWhite, +} + +// validColor will make sure the given color is actually allowed. +func validColor(c string) bool { + return validColors[c] +} + +// Spinner struct to hold the provided options. +type Spinner struct { + mu *sync.RWMutex + Delay time.Duration // Delay is the speed of the indicator + chars []string // chars holds the chosen character set + Prefix string // Prefix is the text preppended to the indicator + Suffix string // Suffix is the text appended to the indicator + FinalMSG string // string displayed after Stop() is called + lastOutput string // last character(set) written + color func(a ...interface{}) string // default color is white + Writer io.Writer // to make testing better, exported so users have access. Use `WithWriter` to update after initialization. + active bool // active holds the state of the spinner + stopChan chan struct{} // stopChan is a channel used to stop the indicator + HideCursor bool // hideCursor determines if the cursor is visible + PreUpdate func(s *Spinner) // will be triggered before every spinner update + PostUpdate func(s *Spinner) // will be triggered after every spinner update +} + +// New provides a pointer to an instance of Spinner with the supplied options. +func New(cs []string, d time.Duration, options ...Option) *Spinner { + s := &Spinner{ + Delay: d, + chars: cs, + color: color.New(color.FgWhite).SprintFunc(), + mu: &sync.RWMutex{}, + Writer: color.Output, + stopChan: make(chan struct{}, 1), + active: false, + HideCursor: true, + } + + for _, option := range options { + option(s) + } + + return s +} + +// Option is a function that takes a spinner and applies +// a given configuration. +type Option func(*Spinner) + +// Options contains fields to configure the spinner. +type Options struct { + Color string + Suffix string + FinalMSG string + HideCursor bool +} + +// WithColor adds the given color to the spinner. +func WithColor(color string) Option { + return func(s *Spinner) { + s.Color(color) + } +} + +// WithSuffix adds the given string to the spinner +// as the suffix. +func WithSuffix(suffix string) Option { + return func(s *Spinner) { + s.Suffix = suffix + } +} + +// WithFinalMSG adds the given string ot the spinner +// as the final message to be written. +func WithFinalMSG(finalMsg string) Option { + return func(s *Spinner) { + s.FinalMSG = finalMsg + } +} + +// WithHiddenCursor hides the cursor +// if hideCursor = true given. +func WithHiddenCursor(hideCursor bool) Option { + return func(s *Spinner) { + s.HideCursor = hideCursor + } +} + +// WithWriter adds the given writer to the spinner. This +// function should be favored over directly assigning to +// the struct value. +func WithWriter(w io.Writer) Option { + return func(s *Spinner) { + s.mu.Lock() + s.Writer = w + s.mu.Unlock() + } +} + +// Active will return whether or not the spinner is currently active. +func (s *Spinner) Active() bool { + return s.active +} + +// Start will start the indicator. +func (s *Spinner) Start() { + s.mu.Lock() + if s.active || !isRunningInTerminal() { + s.mu.Unlock() + return + } + if s.HideCursor && !isWindowsTerminalOnWindows { + // hides the cursor + fmt.Fprint(s.Writer, "\033[?25l") + } + s.active = true + s.mu.Unlock() + + go func() { + for { + for i := 0; i < len(s.chars); i++ { + select { + case <-s.stopChan: + return + default: + s.mu.Lock() + if !s.active { + s.mu.Unlock() + return + } + if !isWindowsTerminalOnWindows { + s.erase() + } + + if s.PreUpdate != nil { + s.PreUpdate(s) + } + + var outColor string + if runtime.GOOS == "windows" { + if s.Writer == os.Stderr { + outColor = fmt.Sprintf("\r%s%s%s", s.Prefix, s.chars[i], s.Suffix) + } else { + outColor = fmt.Sprintf("\r%s%s%s", s.Prefix, s.color(s.chars[i]), s.Suffix) + } + } else { + outColor = fmt.Sprintf("\r%s%s%s", s.Prefix, s.color(s.chars[i]), s.Suffix) + } + outPlain := fmt.Sprintf("\r%s%s%s", s.Prefix, s.chars[i], s.Suffix) + fmt.Fprint(s.Writer, outColor) + s.lastOutput = outPlain + delay := s.Delay + + if s.PostUpdate != nil { + s.PostUpdate(s) + } + + s.mu.Unlock() + time.Sleep(delay) + } + } + } + }() +} + +// Stop stops the indicator. +func (s *Spinner) Stop() { + s.mu.Lock() + defer s.mu.Unlock() + if s.active { + s.active = false + if s.HideCursor && !isWindowsTerminalOnWindows { + // makes the cursor visible + fmt.Fprint(s.Writer, "\033[?25h") + } + s.erase() + if s.FinalMSG != "" { + if isWindowsTerminalOnWindows { + fmt.Fprint(s.Writer, "\r", s.FinalMSG) + } else { + fmt.Fprint(s.Writer, s.FinalMSG) + } + } + s.stopChan <- struct{}{} + } +} + +// Restart will stop and start the indicator. +func (s *Spinner) Restart() { + s.Stop() + s.Start() +} + +// Reverse will reverse the order of the slice assigned to the indicator. +func (s *Spinner) Reverse() { + s.mu.Lock() + for i, j := 0, len(s.chars)-1; i < j; i, j = i+1, j-1 { + s.chars[i], s.chars[j] = s.chars[j], s.chars[i] + } + s.mu.Unlock() +} + +// Color will set the struct field for the given color to be used. The spinner +// will need to be explicitly restarted. +func (s *Spinner) Color(colors ...string) error { + colorAttributes := make([]color.Attribute, len(colors)) + + // Verify colours are valid and place the appropriate attribute in the array + for index, c := range colors { + if !validColor(c) { + return errInvalidColor + } + colorAttributes[index] = colorAttributeMap[c] + } + + s.mu.Lock() + s.color = color.New(colorAttributes...).SprintFunc() + s.mu.Unlock() + return nil +} + +// UpdateSpeed will set the indicator delay to the given value. +func (s *Spinner) UpdateSpeed(d time.Duration) { + s.mu.Lock() + s.Delay = d + s.mu.Unlock() +} + +// UpdateCharSet will change the current character set to the given one. +func (s *Spinner) UpdateCharSet(cs []string) { + s.mu.Lock() + s.chars = cs + s.mu.Unlock() +} + +// erase deletes written characters on the current line. +// Caller must already hold s.lock. +func (s *Spinner) erase() { + n := utf8.RuneCountInString(s.lastOutput) + if runtime.GOOS == "windows" && !isWindowsTerminalOnWindows { + clearString := "\r" + strings.Repeat(" ", n) + "\r" + fmt.Fprint(s.Writer, clearString) + s.lastOutput = "" + return + } + + // Taken from https://en.wikipedia.org/wiki/ANSI_escape_code: + // \r - Carriage return - Moves the cursor to column zero + // \033[K - Erases part of the line. If n is 0 (or missing), clear from + // cursor to the end of the line. If n is 1, clear from cursor to beginning + // of the line. If n is 2, clear entire line. Cursor position does not + // change. + fmt.Fprintf(s.Writer, "\r\033[K") + s.lastOutput = "" +} + +// Lock allows for manual control to lock the spinner. +func (s *Spinner) Lock() { + s.mu.Lock() +} + +// Unlock allows for manual control to unlock the spinner. +func (s *Spinner) Unlock() { + s.mu.Unlock() +} + +// GenerateNumberSequence will generate a slice of integers at the +// provided length and convert them each to a string. +func GenerateNumberSequence(length int) []string { + numSeq := make([]string, length) + for i := 0; i < length; i++ { + numSeq[i] = strconv.Itoa(i) + } + return numSeq +} + +// isRunningInTerminal check if stdout file descriptor is terminal +func isRunningInTerminal() bool { + return isatty.IsTerminal(os.Stdout.Fd()) +} diff --git a/vendor/github.com/charmbracelet/lipgloss/.gitignore b/vendor/github.com/charmbracelet/lipgloss/.gitignore new file mode 100644 index 000000000..53e1c2b75 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/.gitignore @@ -0,0 +1 @@ +ssh_example_ed25519* diff --git a/vendor/github.com/charmbracelet/lipgloss/.golangci-soft.yml b/vendor/github.com/charmbracelet/lipgloss/.golangci-soft.yml new file mode 100644 index 000000000..ef456e060 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/.golangci-soft.yml @@ -0,0 +1,47 @@ +run: + tests: false + +issues: + include: + - EXC0001 + - EXC0005 + - EXC0011 + - EXC0012 + - EXC0013 + + max-issues-per-linter: 0 + max-same-issues: 0 + +linters: + enable: + # - dupl + - exhaustive + # - exhaustivestruct + - goconst + - godot + - godox + - gomnd + - gomoddirectives + - goprintffuncname + - ifshort + # - lll + - misspell + - nakedret + - nestif + - noctx + - nolintlint + - prealloc + - wrapcheck + + # disable default linters, they are already enabled in .golangci.yml + disable: + - deadcode + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - structcheck + - typecheck + - unused + - varcheck diff --git a/vendor/github.com/charmbracelet/lipgloss/.golangci.yml b/vendor/github.com/charmbracelet/lipgloss/.golangci.yml new file mode 100644 index 000000000..a5a91d0d9 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/.golangci.yml @@ -0,0 +1,29 @@ +run: + tests: false + +issues: + include: + - EXC0001 + - EXC0005 + - EXC0011 + - EXC0012 + - EXC0013 + + max-issues-per-linter: 0 + max-same-issues: 0 + +linters: + enable: + - bodyclose + - exportloopref + - goimports + - gosec + - nilerr + - predeclared + - revive + - rowserrcheck + - sqlclosecheck + - tparallel + - unconvert + - unparam + - whitespace diff --git a/vendor/github.com/charmbracelet/lipgloss/LICENSE b/vendor/github.com/charmbracelet/lipgloss/LICENSE new file mode 100644 index 000000000..6f5b1fa62 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021-2023 Charmbracelet, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/charmbracelet/lipgloss/README.md b/vendor/github.com/charmbracelet/lipgloss/README.md new file mode 100644 index 000000000..22defac7c --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/README.md @@ -0,0 +1,564 @@ +Lip Gloss +========= + +

+ Lip Gloss title treatment
+ Latest Release + GoDoc + Build Status + phorm.ai + +

+ +Style definitions for nice terminal layouts. Built with TUIs in mind. + +![Lip Gloss example](https://stuff.charm.sh/lipgloss/lipgloss-example.png) + +Lip Gloss takes an expressive, declarative approach to terminal rendering. +Users familiar with CSS will feel at home with Lip Gloss. + +```go + +import "github.com/charmbracelet/lipgloss" + +var style = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#FAFAFA")). + Background(lipgloss.Color("#7D56F4")). + PaddingTop(2). + PaddingLeft(4). + Width(22) + +fmt.Println(style.Render("Hello, kitty")) +``` + +## Colors + +Lip Gloss supports the following color profiles: + +### ANSI 16 colors (4-bit) + +```go +lipgloss.Color("5") // magenta +lipgloss.Color("9") // red +lipgloss.Color("12") // light blue +``` + +### ANSI 256 Colors (8-bit) + +```go +lipgloss.Color("86") // aqua +lipgloss.Color("201") // hot pink +lipgloss.Color("202") // orange +``` + +### True Color (16,777,216 colors; 24-bit) + +```go +lipgloss.Color("#0000FF") // good ol' 100% blue +lipgloss.Color("#04B575") // a green +lipgloss.Color("#3C3C3C") // a dark gray +``` + +...as well as a 1-bit ASCII profile, which is black and white only. + +The terminal's color profile will be automatically detected, and colors outside +the gamut of the current palette will be automatically coerced to their closest +available value. + + +### Adaptive Colors + +You can also specify color options for light and dark backgrounds: + +```go +lipgloss.AdaptiveColor{Light: "236", Dark: "248"} +``` + +The terminal's background color will automatically be detected and the +appropriate color will be chosen at runtime. + +### Complete Colors + +CompleteColor specifies exact values for truecolor, ANSI256, and ANSI color +profiles. + +```go +lipgloss.CompleteColor{True: "#0000FF", ANSI256: "86", ANSI: "5"} +``` + +Automatic color degradation will not be performed in this case and it will be +based on the color specified. + +### Complete Adaptive Colors + +You can use CompleteColor with AdaptiveColor to specify the exact values for +light and dark backgrounds without automatic color degradation. + +```go +lipgloss.CompleteAdaptiveColor{ + Light: CompleteColor{TrueColor: "#d7ffae", ANSI256: "193", ANSI: "11"}, + Dark: CompleteColor{TrueColor: "#d75fee", ANSI256: "163", ANSI: "5"}, +} +``` + +## Inline Formatting + +Lip Gloss supports the usual ANSI text formatting options: + +```go +var style = lipgloss.NewStyle(). + Bold(true). + Italic(true). + Faint(true). + Blink(true). + Strikethrough(true). + Underline(true). + Reverse(true) +``` + + +## Block-Level Formatting + +Lip Gloss also supports rules for block-level formatting: + +```go +// Padding +var style = lipgloss.NewStyle(). + PaddingTop(2). + PaddingRight(4). + PaddingBottom(2). + PaddingLeft(4) + +// Margins +var style = lipgloss.NewStyle(). + MarginTop(2). + MarginRight(4). + MarginBottom(2). + MarginLeft(4) +``` + +There is also shorthand syntax for margins and padding, which follows the same +format as CSS: + +```go +// 2 cells on all sides +lipgloss.NewStyle().Padding(2) + +// 2 cells on the top and bottom, 4 cells on the left and right +lipgloss.NewStyle().Margin(2, 4) + +// 1 cell on the top, 4 cells on the sides, 2 cells on the bottom +lipgloss.NewStyle().Padding(1, 4, 2) + +// Clockwise, starting from the top: 2 cells on the top, 4 on the right, 3 on +// the bottom, and 1 on the left +lipgloss.NewStyle().Margin(2, 4, 3, 1) +``` + + +## Aligning Text + +You can align paragraphs of text to the left, right, or center. + +```go +var style = lipgloss.NewStyle(). + Width(24). + Align(lipgloss.Left). // align it left + Align(lipgloss.Right). // no wait, align it right + Align(lipgloss.Center) // just kidding, align it in the center +``` + + +## Width and Height + +Setting a minimum width and height is simple and straightforward. + +```go +var style = lipgloss.NewStyle(). + SetString("What’s for lunch?"). + Width(24). + Height(32). + Foreground(lipgloss.Color("63")) +``` + + +## Borders + +Adding borders is easy: + +```go +// Add a purple, rectangular border +var style = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("63")) + +// Set a rounded, yellow-on-purple border to the top and left +var anotherStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("228")). + BorderBackground(lipgloss.Color("63")). + BorderTop(true). + BorderLeft(true) + +// Make your own border +var myCuteBorder = lipgloss.Border{ + Top: "._.:*:", + Bottom: "._.:*:", + Left: "|*", + Right: "|*", + TopLeft: "*", + TopRight: "*", + BottomLeft: "*", + BottomRight: "*", +} +``` + +There are also shorthand functions for defining borders, which follow a similar +pattern to the margin and padding shorthand functions. + +```go +// Add a thick border to the top and bottom +lipgloss.NewStyle(). + Border(lipgloss.ThickBorder(), true, false) + +// Add a double border to the top and left sides. Rules are set clockwise +// from top. +lipgloss.NewStyle(). + Border(lipgloss.DoubleBorder(), true, false, false, true) +``` + +For more on borders see [the docs][docs]. + + +## Copying Styles + +Just use `Copy()`: + +```go +var style = lipgloss.NewStyle().Foreground(lipgloss.Color("219")) + +var wildStyle = style.Copy().Blink(true) +``` + +`Copy()` performs a copy on the underlying data structure ensuring that you get +a true, dereferenced copy of a style. Without copying, it's possible to mutate +styles. + + +## Inheritance + +Styles can inherit rules from other styles. When inheriting, only unset rules +on the receiver are inherited. + +```go +var styleA = lipgloss.NewStyle(). + Foreground(lipgloss.Color("229")). + Background(lipgloss.Color("63")) + +// Only the background color will be inherited here, because the foreground +// color will have been already set: +var styleB = lipgloss.NewStyle(). + Foreground(lipgloss.Color("201")). + Inherit(styleA) +``` + + +## Unsetting Rules + +All rules can be unset: + +```go +var style = lipgloss.NewStyle(). + Bold(true). // make it bold + UnsetBold(). // jk don't make it bold + Background(lipgloss.Color("227")). // yellow background + UnsetBackground() // never mind +``` + +When a rule is unset, it won't be inherited or copied. + + +## Enforcing Rules + +Sometimes, such as when developing a component, you want to make sure style +definitions respect their intended purpose in the UI. This is where `Inline` +and `MaxWidth`, and `MaxHeight` come in: + +```go +// Force rendering onto a single line, ignoring margins, padding, and borders. +someStyle.Inline(true).Render("yadda yadda") + +// Also limit rendering to five cells +someStyle.Inline(true).MaxWidth(5).Render("yadda yadda") + +// Limit rendering to a 5x5 cell block +someStyle.MaxWidth(5).MaxHeight(5).Render("yadda yadda") +``` + +## Tabs + +The tab character (`\t`) is rendered differently in different terminals (often +as 8 spaces, sometimes 4). Because of this inconsistency, Lip Gloss converts +tabs to 4 spaces at render time. This behavior can be changed on a per-style +basis, however: + +```go +style := lipgloss.NewStyle() // tabs will render as 4 spaces, the default +style = style.TabWidth(2) // render tabs as 2 spaces +style = style.TabWidth(0) // remove tabs entirely +style = style.TabWidth(lipgloss.NoTabConversion) // leave tabs intact +``` + +## Rendering + +Generally, you just call the `Render(string...)` method on a `lipgloss.Style`: + +```go +style := lipgloss.NewStyle().Bold(true).SetString("Hello,") +fmt.Println(style.Render("kitty.")) // Hello, kitty. +fmt.Println(style.Render("puppy.")) // Hello, puppy. +``` + +But you could also use the Stringer interface: + +```go +var style = lipgloss.NewStyle().SetString("你好,猫咪。").Bold(true) +fmt.Println(style) // 你好,猫咪。 +``` + +### Custom Renderers + +Custom renderers allow you to render to a specific outputs. This is +particularly important when you want to render to different outputs and +correctly detect the color profile and dark background status for each, such as +in a server-client situation. + +```go +func myLittleHandler(sess ssh.Session) { + // Create a renderer for the client. + renderer := lipgloss.NewRenderer(sess) + + // Create a new style on the renderer. + style := renderer.NewStyle().Background(lipgloss.AdaptiveColor{Light: "63", Dark: "228"}) + + // Render. The color profile and dark background state will be correctly detected. + io.WriteString(sess, style.Render("Heyyyyyyy")) +} +``` + +For an example on using a custom renderer over SSH with [Wish][wish] see the +[SSH example][ssh-example]. + +## Utilities + +In addition to pure styling, Lip Gloss also ships with some utilities to help +assemble your layouts. + + +### Joining Paragraphs + +Horizontally and vertically joining paragraphs is a cinch. + +```go +// Horizontally join three paragraphs along their bottom edges +lipgloss.JoinHorizontal(lipgloss.Bottom, paragraphA, paragraphB, paragraphC) + +// Vertically join two paragraphs along their center axes +lipgloss.JoinVertical(lipgloss.Center, paragraphA, paragraphB) + +// Horizontally join three paragraphs, with the shorter ones aligning 20% +// from the top of the tallest +lipgloss.JoinHorizontal(0.2, paragraphA, paragraphB, paragraphC) +``` + + +### Measuring Width and Height + +Sometimes you’ll want to know the width and height of text blocks when building +your layouts. + +```go +// Render a block of text. +var style = lipgloss.NewStyle(). + Width(40). + Padding(2) +var block string = style.Render(someLongString) + +// Get the actual, physical dimensions of the text block. +width := lipgloss.Width(block) +height := lipgloss.Height(block) + +// Here's a shorthand function. +w, h := lipgloss.Size(block) +``` + +### Placing Text in Whitespace + +Sometimes you’ll simply want to place a block of text in whitespace. + +```go +// Center a paragraph horizontally in a space 80 cells wide. The height of +// the block returned will be as tall as the input paragraph. +block := lipgloss.PlaceHorizontal(80, lipgloss.Center, fancyStyledParagraph) + +// Place a paragraph at the bottom of a space 30 cells tall. The width of +// the text block returned will be as wide as the input paragraph. +block := lipgloss.PlaceVertical(30, lipgloss.Bottom, fancyStyledParagraph) + +// Place a paragraph in the bottom right corner of a 30x80 cell space. +block := lipgloss.Place(30, 80, lipgloss.Right, lipgloss.Bottom, fancyStyledParagraph) +``` + +You can also style the whitespace. For details, see [the docs][docs]. + +### Rendering Tables + +Lip Gloss ships with a table rendering sub-package. + +```go +import "github.com/charmbracelet/lipgloss/table" +``` + +Define some rows of data. + +```go +rows := [][]string{ + {"Chinese", "您好", "你好"}, + {"Japanese", "こんにちは", "やあ"}, + {"Arabic", "أهلين", "أهلا"}, + {"Russian", "Здравствуйте", "Привет"}, + {"Spanish", "Hola", "¿Qué tal?"}, +} +``` + +Use the table package to style and render the table. + +```go +t := table.New(). + Border(lipgloss.NormalBorder()). + BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("99"))). + StyleFunc(func(row, col int) lipgloss.Style { + switch { + case row == 0: + return HeaderStyle + case row%2 == 0: + return EvenRowStyle + default: + return OddRowStyle + } + }). + Headers("LANGUAGE", "FORMAL", "INFORMAL"). + Rows(rows...) + +// You can also add tables row-by-row +t.Row("English", "You look absolutely fabulous.", "How's it going?") +``` + +Print the table. + +```go +fmt.Println(t) +``` + +![Table Example](https://github.com/charmbracelet/lipgloss/assets/42545625/6e4b70c4-f494-45da-a467-bdd27df30d5d) + +For more on tables see [the docs](https://pkg.go.dev/github.com/charmbracelet/lipgloss?tab=doc) and [examples](https://github.com/charmbracelet/lipgloss/tree/master/examples/table). + +*** + +## FAQ + +
+ +Why are things misaligning? Why are borders at the wrong widths? + +

This is most likely due to your locale and encoding, particularly with +regard to Chinese, Japanese, and Korean (for example, zh_CN.UTF-8 +or ja_JP.UTF-8). The most direct way to fix this is to set +RUNEWIDTH_EASTASIAN=0 in your environment.

+ +

For details see https://github.com/charmbracelet/lipgloss/issues/40.

+
+ +
+ +Why isn't Lip Gloss displaying colors? + +

Lip Gloss automatically degrades colors to the best available option in the +given terminal, and if output's not a TTY it will remove color output entirely. +This is common when running tests, CI, or when piping output elsewhere.

+ +

If necessary, you can force a color profile in your tests with +SetColorProfile.

+ +```go +import ( + "github.com/charmbracelet/lipgloss" + "github.com/muesli/termenv" +) + +lipgloss.SetColorProfile(termenv.TrueColor) +``` + +*Note:* this option limits the flexibility of your application and can cause +ANSI escape codes to be output in cases where that might not be desired. Take +careful note of your use case and environment before choosing to force a color +profile. +
+ +## What about [Bubble Tea][tea]? + +Lip Gloss doesn’t replace Bubble Tea. Rather, it is an excellent Bubble Tea +companion. It was designed to make assembling terminal user interface views as +simple and fun as possible so that you can focus on building your application +instead of concerning yourself with low-level layout details. + +In simple terms, you can use Lip Gloss to help build your Bubble Tea views. + +[tea]: https://github.com/charmbracelet/tea + + +## Under the Hood + +Lip Gloss is built on the excellent [Termenv][termenv] and [Reflow][reflow] +libraries which deal with color and ANSI-aware text operations, respectively. +For many use cases Termenv and Reflow will be sufficient for your needs. + +[termenv]: https://github.com/muesli/termenv +[reflow]: https://github.com/muesli/reflow + + +## Rendering Markdown + +For a more document-centric rendering solution with support for things like +lists, tables, and syntax-highlighted code have a look at [Glamour][glamour], +the stylesheet-based Markdown renderer. + +[glamour]: https://github.com/charmbracelet/glamour + + +## Feedback + +We’d love to hear your thoughts on this project. Feel free to drop us a note! + +* [Twitter](https://twitter.com/charmcli) +* [The Fediverse](https://mastodon.social/@charmcli) +* [Discord](https://charm.sh/chat) + +## License + +[MIT](https://github.com/charmbracelet/lipgloss/raw/master/LICENSE) + +*** + +Part of [Charm](https://charm.sh). + +The Charm logo + +Charm热爱开源 • Charm loves open source + + +[docs]: https://pkg.go.dev/github.com/charmbracelet/lipgloss?tab=doc +[wish]: https://github.com/charmbracelet/wish +[ssh-example]: examples/ssh diff --git a/vendor/github.com/charmbracelet/lipgloss/align.go b/vendor/github.com/charmbracelet/lipgloss/align.go new file mode 100644 index 000000000..4aec37175 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/align.go @@ -0,0 +1,83 @@ +package lipgloss + +import ( + "strings" + + "github.com/charmbracelet/x/exp/term/ansi" + "github.com/muesli/termenv" +) + +// Perform text alignment. If the string is multi-lined, we also make all lines +// the same width by padding them with spaces. If a termenv style is passed, +// use that to style the spaces added. +func alignTextHorizontal(str string, pos Position, width int, style *termenv.Style) string { + lines, widestLine := getLines(str) + var b strings.Builder + + for i, l := range lines { + lineWidth := ansi.StringWidth(l) + + shortAmount := widestLine - lineWidth // difference from the widest line + shortAmount += max(0, width-(shortAmount+lineWidth)) // difference from the total width, if set + + if shortAmount > 0 { + switch pos { //nolint:exhaustive + case Right: + s := strings.Repeat(" ", shortAmount) + if style != nil { + s = style.Styled(s) + } + l = s + l + case Center: + // Note: remainder goes on the right. + left := shortAmount / 2 //nolint:gomnd + right := left + shortAmount%2 //nolint:gomnd + + leftSpaces := strings.Repeat(" ", left) + rightSpaces := strings.Repeat(" ", right) + + if style != nil { + leftSpaces = style.Styled(leftSpaces) + rightSpaces = style.Styled(rightSpaces) + } + l = leftSpaces + l + rightSpaces + default: // Left + s := strings.Repeat(" ", shortAmount) + if style != nil { + s = style.Styled(s) + } + l += s + } + } + + b.WriteString(l) + if i < len(lines)-1 { + b.WriteRune('\n') + } + } + + return b.String() +} + +func alignTextVertical(str string, pos Position, height int, _ *termenv.Style) string { + strHeight := strings.Count(str, "\n") + 1 + if height < strHeight { + return str + } + + switch pos { + case Top: + return str + strings.Repeat("\n", height-strHeight) + case Center: + topPadding, bottomPadding := (height-strHeight)/2, (height-strHeight)/2 //nolint:gomnd + if strHeight+topPadding+bottomPadding > height { + topPadding-- + } else if strHeight+topPadding+bottomPadding < height { + bottomPadding++ + } + return strings.Repeat("\n", topPadding) + str + strings.Repeat("\n", bottomPadding) + case Bottom: + return strings.Repeat("\n", height-strHeight) + str + } + return str +} diff --git a/vendor/github.com/charmbracelet/lipgloss/ansi_unix.go b/vendor/github.com/charmbracelet/lipgloss/ansi_unix.go new file mode 100644 index 000000000..d416b8c99 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/ansi_unix.go @@ -0,0 +1,7 @@ +//go:build !windows +// +build !windows + +package lipgloss + +// enableLegacyWindowsANSI is only needed on Windows. +func enableLegacyWindowsANSI() {} diff --git a/vendor/github.com/charmbracelet/lipgloss/ansi_windows.go b/vendor/github.com/charmbracelet/lipgloss/ansi_windows.go new file mode 100644 index 000000000..0cf56e4c7 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/ansi_windows.go @@ -0,0 +1,22 @@ +//go:build windows +// +build windows + +package lipgloss + +import ( + "sync" + + "github.com/muesli/termenv" +) + +var enableANSI sync.Once + +// enableANSIColors enables support for ANSI color sequences in the Windows +// default console (cmd.exe and the PowerShell application). Note that this +// only works with Windows 10. Also note that Windows Terminal supports colors +// by default. +func enableLegacyWindowsANSI() { + enableANSI.Do(func() { + _, _ = termenv.EnableWindowsANSIConsole() + }) +} diff --git a/vendor/github.com/charmbracelet/lipgloss/borders.go b/vendor/github.com/charmbracelet/lipgloss/borders.go new file mode 100644 index 000000000..38f875f1c --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/borders.go @@ -0,0 +1,443 @@ +package lipgloss + +import ( + "strings" + + "github.com/charmbracelet/x/exp/term/ansi" + "github.com/muesli/termenv" + "github.com/rivo/uniseg" +) + +// Border contains a series of values which comprise the various parts of a +// border. +type Border struct { + Top string + Bottom string + Left string + Right string + TopLeft string + TopRight string + BottomLeft string + BottomRight string + MiddleLeft string + MiddleRight string + Middle string + MiddleTop string + MiddleBottom string +} + +// GetTopSize returns the width of the top border. If borders contain runes of +// varying widths, the widest rune is returned. If no border exists on the top +// edge, 0 is returned. +func (b Border) GetTopSize() int { + return getBorderEdgeWidth(b.TopLeft, b.Top, b.TopRight) +} + +// GetRightSize returns the width of the right border. If borders contain +// runes of varying widths, the widest rune is returned. If no border exists on +// the right edge, 0 is returned. +func (b Border) GetRightSize() int { + return getBorderEdgeWidth(b.TopRight, b.Right, b.BottomRight) +} + +// GetBottomSize returns the width of the bottom border. If borders contain +// runes of varying widths, the widest rune is returned. If no border exists on +// the bottom edge, 0 is returned. +func (b Border) GetBottomSize() int { + return getBorderEdgeWidth(b.BottomLeft, b.Bottom, b.BottomRight) +} + +// GetLeftSize returns the width of the left border. If borders contain runes +// of varying widths, the widest rune is returned. If no border exists on the +// left edge, 0 is returned. +func (b Border) GetLeftSize() int { + return getBorderEdgeWidth(b.TopLeft, b.Left, b.BottomLeft) +} + +func getBorderEdgeWidth(borderParts ...string) (maxWidth int) { + for _, piece := range borderParts { + w := maxRuneWidth(piece) + if w > maxWidth { + maxWidth = w + } + } + return maxWidth +} + +var ( + noBorder = Border{} + + normalBorder = Border{ + Top: "─", + Bottom: "─", + Left: "│", + Right: "│", + TopLeft: "┌", + TopRight: "┐", + BottomLeft: "└", + BottomRight: "┘", + MiddleLeft: "├", + MiddleRight: "┤", + Middle: "┼", + MiddleTop: "┬", + MiddleBottom: "┴", + } + + roundedBorder = Border{ + Top: "─", + Bottom: "─", + Left: "│", + Right: "│", + TopLeft: "╭", + TopRight: "╮", + BottomLeft: "╰", + BottomRight: "╯", + MiddleLeft: "├", + MiddleRight: "┤", + Middle: "┼", + MiddleTop: "┬", + MiddleBottom: "┴", + } + + blockBorder = Border{ + Top: "█", + Bottom: "█", + Left: "█", + Right: "█", + TopLeft: "█", + TopRight: "█", + BottomLeft: "█", + BottomRight: "█", + } + + outerHalfBlockBorder = Border{ + Top: "▀", + Bottom: "▄", + Left: "▌", + Right: "▐", + TopLeft: "▛", + TopRight: "▜", + BottomLeft: "▙", + BottomRight: "▟", + } + + innerHalfBlockBorder = Border{ + Top: "▄", + Bottom: "▀", + Left: "▐", + Right: "▌", + TopLeft: "▗", + TopRight: "▖", + BottomLeft: "▝", + BottomRight: "▘", + } + + thickBorder = Border{ + Top: "━", + Bottom: "━", + Left: "┃", + Right: "┃", + TopLeft: "┏", + TopRight: "┓", + BottomLeft: "┗", + BottomRight: "┛", + MiddleLeft: "┣", + MiddleRight: "┫", + Middle: "╋", + MiddleTop: "┳", + MiddleBottom: "┻", + } + + doubleBorder = Border{ + Top: "═", + Bottom: "═", + Left: "║", + Right: "║", + TopLeft: "╔", + TopRight: "╗", + BottomLeft: "╚", + BottomRight: "╝", + MiddleLeft: "╠", + MiddleRight: "╣", + Middle: "╬", + MiddleTop: "╦", + MiddleBottom: "╩", + } + + hiddenBorder = Border{ + Top: " ", + Bottom: " ", + Left: " ", + Right: " ", + TopLeft: " ", + TopRight: " ", + BottomLeft: " ", + BottomRight: " ", + MiddleLeft: " ", + MiddleRight: " ", + Middle: " ", + MiddleTop: " ", + MiddleBottom: " ", + } +) + +// NormalBorder returns a standard-type border with a normal weight and 90 +// degree corners. +func NormalBorder() Border { + return normalBorder +} + +// RoundedBorder returns a border with rounded corners. +func RoundedBorder() Border { + return roundedBorder +} + +// BlockBorder returns a border that takes the whole block. +func BlockBorder() Border { + return blockBorder +} + +// OuterHalfBlockBorder returns a half-block border that sits outside the frame. +func OuterHalfBlockBorder() Border { + return outerHalfBlockBorder +} + +// InnerHalfBlockBorder returns a half-block border that sits inside the frame. +func InnerHalfBlockBorder() Border { + return innerHalfBlockBorder +} + +// ThickBorder returns a border that's thicker than the one returned by +// NormalBorder. +func ThickBorder() Border { + return thickBorder +} + +// DoubleBorder returns a border comprised of two thin strokes. +func DoubleBorder() Border { + return doubleBorder +} + +// HiddenBorder returns a border that renders as a series of single-cell +// spaces. It's useful for cases when you want to remove a standard border but +// maintain layout positioning. This said, you can still apply a background +// color to a hidden border. +func HiddenBorder() Border { + return hiddenBorder +} + +func (s Style) applyBorder(str string) string { + var ( + topSet = s.isSet(borderTopKey) + rightSet = s.isSet(borderRightKey) + bottomSet = s.isSet(borderBottomKey) + leftSet = s.isSet(borderLeftKey) + + border = s.getBorderStyle() + hasTop = s.getAsBool(borderTopKey, false) + hasRight = s.getAsBool(borderRightKey, false) + hasBottom = s.getAsBool(borderBottomKey, false) + hasLeft = s.getAsBool(borderLeftKey, false) + + topFG = s.getAsColor(borderTopForegroundKey) + rightFG = s.getAsColor(borderRightForegroundKey) + bottomFG = s.getAsColor(borderBottomForegroundKey) + leftFG = s.getAsColor(borderLeftForegroundKey) + + topBG = s.getAsColor(borderTopBackgroundKey) + rightBG = s.getAsColor(borderRightBackgroundKey) + bottomBG = s.getAsColor(borderBottomBackgroundKey) + leftBG = s.getAsColor(borderLeftBackgroundKey) + ) + + // If a border is set and no sides have been specifically turned on or off + // render borders on all sides. + if border != noBorder && !(topSet || rightSet || bottomSet || leftSet) { + hasTop = true + hasRight = true + hasBottom = true + hasLeft = true + } + + // If no border is set or all borders are been disabled, abort. + if border == noBorder || (!hasTop && !hasRight && !hasBottom && !hasLeft) { + return str + } + + lines, width := getLines(str) + + if hasLeft { + if border.Left == "" { + border.Left = " " + } + width += maxRuneWidth(border.Left) + } + + if hasRight && border.Right == "" { + border.Right = " " + } + + // If corners should be rendered but are set with the empty string, fill them + // with a single space. + if hasTop && hasLeft && border.TopLeft == "" { + border.TopLeft = " " + } + if hasTop && hasRight && border.TopRight == "" { + border.TopRight = " " + } + if hasBottom && hasLeft && border.BottomLeft == "" { + border.BottomLeft = " " + } + if hasBottom && hasRight && border.BottomRight == "" { + border.BottomRight = " " + } + + // Figure out which corners we should actually be using based on which + // sides are set to show. + if hasTop { + switch { + case !hasLeft && !hasRight: + border.TopLeft = "" + border.TopRight = "" + case !hasLeft: + border.TopLeft = "" + case !hasRight: + border.TopRight = "" + } + } + if hasBottom { + switch { + case !hasLeft && !hasRight: + border.BottomLeft = "" + border.BottomRight = "" + case !hasLeft: + border.BottomLeft = "" + case !hasRight: + border.BottomRight = "" + } + } + + // For now, limit corners to one rune. + border.TopLeft = getFirstRuneAsString(border.TopLeft) + border.TopRight = getFirstRuneAsString(border.TopRight) + border.BottomRight = getFirstRuneAsString(border.BottomRight) + border.BottomLeft = getFirstRuneAsString(border.BottomLeft) + + var out strings.Builder + + // Render top + if hasTop { + top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width) + top = s.styleBorder(top, topFG, topBG) + out.WriteString(top) + out.WriteRune('\n') + } + + leftRunes := []rune(border.Left) + leftIndex := 0 + + rightRunes := []rune(border.Right) + rightIndex := 0 + + // Render sides + for i, l := range lines { + if hasLeft { + r := string(leftRunes[leftIndex]) + leftIndex++ + if leftIndex >= len(leftRunes) { + leftIndex = 0 + } + out.WriteString(s.styleBorder(r, leftFG, leftBG)) + } + out.WriteString(l) + if hasRight { + r := string(rightRunes[rightIndex]) + rightIndex++ + if rightIndex >= len(rightRunes) { + rightIndex = 0 + } + out.WriteString(s.styleBorder(r, rightFG, rightBG)) + } + if i < len(lines)-1 { + out.WriteRune('\n') + } + } + + // Render bottom + if hasBottom { + bottom := renderHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, width) + bottom = s.styleBorder(bottom, bottomFG, bottomBG) + out.WriteRune('\n') + out.WriteString(bottom) + } + + return out.String() +} + +// Render the horizontal (top or bottom) portion of a border. +func renderHorizontalEdge(left, middle, right string, width int) string { + if middle == "" { + middle = " " + } + + leftWidth := ansi.StringWidth(left) + rightWidth := ansi.StringWidth(right) + + runes := []rune(middle) + j := 0 + + out := strings.Builder{} + out.WriteString(left) + for i := leftWidth + rightWidth; i < width+rightWidth; { + out.WriteRune(runes[j]) + j++ + if j >= len(runes) { + j = 0 + } + i += ansi.StringWidth(string(runes[j])) + } + out.WriteString(right) + + return out.String() +} + +// Apply foreground and background styling to a border. +func (s Style) styleBorder(border string, fg, bg TerminalColor) string { + if fg == noColor && bg == noColor { + return border + } + + style := termenv.Style{} + + if fg != noColor { + style = style.Foreground(fg.color(s.r)) + } + if bg != noColor { + style = style.Background(bg.color(s.r)) + } + + return style.Styled(border) +} + +func maxRuneWidth(str string) int { + var width int + + state := -1 + for len(str) > 0 { + var w int + _, str, w, state = uniseg.FirstGraphemeClusterInString(str, state) + if w > width { + width = w + } + } + + return width +} + +func getFirstRuneAsString(str string) string { + if str == "" { + return str + } + r := []rune(str) + return string(r[0]) +} diff --git a/vendor/github.com/charmbracelet/lipgloss/color.go b/vendor/github.com/charmbracelet/lipgloss/color.go new file mode 100644 index 000000000..43f5b434b --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/color.go @@ -0,0 +1,172 @@ +package lipgloss + +import ( + "strconv" + + "github.com/muesli/termenv" +) + +// TerminalColor is a color intended to be rendered in the terminal. +type TerminalColor interface { + color(*Renderer) termenv.Color + RGBA() (r, g, b, a uint32) +} + +var noColor = NoColor{} + +// NoColor is used to specify the absence of color styling. When this is active +// foreground colors will be rendered with the terminal's default text color, +// and background colors will not be drawn at all. +// +// Example usage: +// +// var style = someStyle.Copy().Background(lipgloss.NoColor{}) +type NoColor struct{} + +func (NoColor) color(*Renderer) termenv.Color { + return termenv.NoColor{} +} + +// RGBA returns the RGBA value of this color. Because we have to return +// something, despite this color being the absence of color, we're returning +// black with 100% opacity. +// +// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. +// +// Deprecated. +func (n NoColor) RGBA() (r, g, b, a uint32) { + return 0x0, 0x0, 0x0, 0xFFFF //nolint:gomnd +} + +// Color specifies a color by hex or ANSI value. For example: +// +// ansiColor := lipgloss.Color("21") +// hexColor := lipgloss.Color("#0000ff") +type Color string + +func (c Color) color(r *Renderer) termenv.Color { + return r.ColorProfile().Color(string(c)) +} + +// RGBA returns the RGBA value of this color. This satisfies the Go Color +// interface. Note that on error we return black with 100% opacity, or: +// +// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. +// +// Deprecated. +func (c Color) RGBA() (r, g, b, a uint32) { + return termenv.ConvertToRGB(c.color(renderer)).RGBA() +} + +// ANSIColor is a color specified by an ANSI color value. It's merely syntactic +// sugar for the more general Color function. Invalid colors will render as +// black. +// +// Example usage: +// +// // These two statements are equivalent. +// colorA := lipgloss.ANSIColor(21) +// colorB := lipgloss.Color("21") +type ANSIColor uint + +func (ac ANSIColor) color(r *Renderer) termenv.Color { + return Color(strconv.FormatUint(uint64(ac), 10)).color(r) +} + +// RGBA returns the RGBA value of this color. This satisfies the Go Color +// interface. Note that on error we return black with 100% opacity, or: +// +// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. +// +// Deprecated. +func (ac ANSIColor) RGBA() (r, g, b, a uint32) { + cf := Color(strconv.FormatUint(uint64(ac), 10)) + return cf.RGBA() +} + +// AdaptiveColor provides color options for light and dark backgrounds. The +// appropriate color will be returned at runtime based on the darkness of the +// terminal background color. +// +// Example usage: +// +// color := lipgloss.AdaptiveColor{Light: "#0000ff", Dark: "#000099"} +type AdaptiveColor struct { + Light string + Dark string +} + +func (ac AdaptiveColor) color(r *Renderer) termenv.Color { + if r.HasDarkBackground() { + return Color(ac.Dark).color(r) + } + return Color(ac.Light).color(r) +} + +// RGBA returns the RGBA value of this color. This satisfies the Go Color +// interface. Note that on error we return black with 100% opacity, or: +// +// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. +// +// Deprecated. +func (ac AdaptiveColor) RGBA() (r, g, b, a uint32) { + return termenv.ConvertToRGB(ac.color(renderer)).RGBA() +} + +// CompleteColor specifies exact values for truecolor, ANSI256, and ANSI color +// profiles. Automatic color degradation will not be performed. +type CompleteColor struct { + TrueColor string + ANSI256 string + ANSI string +} + +func (c CompleteColor) color(r *Renderer) termenv.Color { + p := r.ColorProfile() + switch p { //nolint:exhaustive + case termenv.TrueColor: + return p.Color(c.TrueColor) + case termenv.ANSI256: + return p.Color(c.ANSI256) + case termenv.ANSI: + return p.Color(c.ANSI) + default: + return termenv.NoColor{} + } +} + +// RGBA returns the RGBA value of this color. This satisfies the Go Color +// interface. Note that on error we return black with 100% opacity, or: +// +// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. +// CompleteAdaptiveColor specifies exact values for truecolor, ANSI256, and ANSI color +// +// Deprecated. +func (c CompleteColor) RGBA() (r, g, b, a uint32) { + return termenv.ConvertToRGB(c.color(renderer)).RGBA() +} + +// CompleteAdaptiveColor specifies exact values for truecolor, ANSI256, and ANSI color +// profiles, with separate options for light and dark backgrounds. Automatic +// color degradation will not be performed. +type CompleteAdaptiveColor struct { + Light CompleteColor + Dark CompleteColor +} + +func (cac CompleteAdaptiveColor) color(r *Renderer) termenv.Color { + if r.HasDarkBackground() { + return cac.Dark.color(r) + } + return cac.Light.color(r) +} + +// RGBA returns the RGBA value of this color. This satisfies the Go Color +// interface. Note that on error we return black with 100% opacity, or: +// +// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. +// +// Deprecated. +func (cac CompleteAdaptiveColor) RGBA() (r, g, b, a uint32) { + return termenv.ConvertToRGB(cac.color(renderer)).RGBA() +} diff --git a/vendor/github.com/charmbracelet/lipgloss/get.go b/vendor/github.com/charmbracelet/lipgloss/get.go new file mode 100644 index 000000000..d6c83a971 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/get.go @@ -0,0 +1,502 @@ +package lipgloss + +import ( + "strings" + + "github.com/charmbracelet/x/exp/term/ansi" +) + +// GetBold returns the style's bold value. If no value is set false is returned. +func (s Style) GetBold() bool { + return s.getAsBool(boldKey, false) +} + +// GetItalic returns the style's italic value. If no value is set false is +// returned. +func (s Style) GetItalic() bool { + return s.getAsBool(italicKey, false) +} + +// GetUnderline returns the style's underline value. If no value is set false is +// returned. +func (s Style) GetUnderline() bool { + return s.getAsBool(underlineKey, false) +} + +// GetStrikethrough returns the style's strikethrough value. If no value is set false +// is returned. +func (s Style) GetStrikethrough() bool { + return s.getAsBool(strikethroughKey, false) +} + +// GetReverse returns the style's reverse value. If no value is set false is +// returned. +func (s Style) GetReverse() bool { + return s.getAsBool(reverseKey, false) +} + +// GetBlink returns the style's blink value. If no value is set false is +// returned. +func (s Style) GetBlink() bool { + return s.getAsBool(blinkKey, false) +} + +// GetFaint returns the style's faint value. If no value is set false is +// returned. +func (s Style) GetFaint() bool { + return s.getAsBool(faintKey, false) +} + +// GetForeground returns the style's foreground color. If no value is set +// NoColor{} is returned. +func (s Style) GetForeground() TerminalColor { + return s.getAsColor(foregroundKey) +} + +// GetBackground returns the style's background color. If no value is set +// NoColor{} is returned. +func (s Style) GetBackground() TerminalColor { + return s.getAsColor(backgroundKey) +} + +// GetWidth returns the style's width setting. If no width is set 0 is +// returned. +func (s Style) GetWidth() int { + return s.getAsInt(widthKey) +} + +// GetHeight returns the style's height setting. If no height is set 0 is +// returned. +func (s Style) GetHeight() int { + return s.getAsInt(heightKey) +} + +// GetAlign returns the style's implicit horizontal alignment setting. +// If no alignment is set Position.Left is returned. +func (s Style) GetAlign() Position { + v := s.getAsPosition(alignHorizontalKey) + if v == Position(0) { + return Left + } + return v +} + +// GetAlignHorizontal returns the style's implicit horizontal alignment setting. +// If no alignment is set Position.Left is returned. +func (s Style) GetAlignHorizontal() Position { + v := s.getAsPosition(alignHorizontalKey) + if v == Position(0) { + return Left + } + return v +} + +// GetAlignVertical returns the style's implicit vertical alignment setting. +// If no alignment is set Position.Top is returned. +func (s Style) GetAlignVertical() Position { + v := s.getAsPosition(alignVerticalKey) + if v == Position(0) { + return Top + } + return v +} + +// GetPadding returns the style's top, right, bottom, and left padding values, +// in that order. 0 is returned for unset values. +func (s Style) GetPadding() (top, right, bottom, left int) { + return s.getAsInt(paddingTopKey), + s.getAsInt(paddingRightKey), + s.getAsInt(paddingBottomKey), + s.getAsInt(paddingLeftKey) +} + +// GetPaddingTop returns the style's top padding. If no value is set 0 is +// returned. +func (s Style) GetPaddingTop() int { + return s.getAsInt(paddingTopKey) +} + +// GetPaddingRight returns the style's right padding. If no value is set 0 is +// returned. +func (s Style) GetPaddingRight() int { + return s.getAsInt(paddingRightKey) +} + +// GetPaddingBottom returns the style's bottom padding. If no value is set 0 is +// returned. +func (s Style) GetPaddingBottom() int { + return s.getAsInt(paddingBottomKey) +} + +// GetPaddingLeft returns the style's left padding. If no value is set 0 is +// returned. +func (s Style) GetPaddingLeft() int { + return s.getAsInt(paddingLeftKey) +} + +// GetHorizontalPadding returns the style's left and right padding. Unset +// values are measured as 0. +func (s Style) GetHorizontalPadding() int { + return s.getAsInt(paddingLeftKey) + s.getAsInt(paddingRightKey) +} + +// GetVerticalPadding returns the style's top and bottom padding. Unset values +// are measured as 0. +func (s Style) GetVerticalPadding() int { + return s.getAsInt(paddingTopKey) + s.getAsInt(paddingBottomKey) +} + +// GetColorWhitespace returns the style's whitespace coloring setting. If no +// value is set false is returned. +func (s Style) GetColorWhitespace() bool { + return s.getAsBool(colorWhitespaceKey, false) +} + +// GetMargin returns the style's top, right, bottom, and left margins, in that +// order. 0 is returned for unset values. +func (s Style) GetMargin() (top, right, bottom, left int) { + return s.getAsInt(marginTopKey), + s.getAsInt(marginRightKey), + s.getAsInt(marginBottomKey), + s.getAsInt(marginLeftKey) +} + +// GetMarginTop returns the style's top margin. If no value is set 0 is +// returned. +func (s Style) GetMarginTop() int { + return s.getAsInt(marginTopKey) +} + +// GetMarginRight returns the style's right margin. If no value is set 0 is +// returned. +func (s Style) GetMarginRight() int { + return s.getAsInt(marginRightKey) +} + +// GetMarginBottom returns the style's bottom margin. If no value is set 0 is +// returned. +func (s Style) GetMarginBottom() int { + return s.getAsInt(marginBottomKey) +} + +// GetMarginLeft returns the style's left margin. If no value is set 0 is +// returned. +func (s Style) GetMarginLeft() int { + return s.getAsInt(marginLeftKey) +} + +// GetHorizontalMargins returns the style's left and right margins. Unset +// values are measured as 0. +func (s Style) GetHorizontalMargins() int { + return s.getAsInt(marginLeftKey) + s.getAsInt(marginRightKey) +} + +// GetVerticalMargins returns the style's top and bottom margins. Unset values +// are measured as 0. +func (s Style) GetVerticalMargins() int { + return s.getAsInt(marginTopKey) + s.getAsInt(marginBottomKey) +} + +// GetBorder returns the style's border style (type Border) and value for the +// top, right, bottom, and left in that order. If no value is set for the +// border style, Border{} is returned. For all other unset values false is +// returned. +func (s Style) GetBorder() (b Border, top, right, bottom, left bool) { + return s.getBorderStyle(), + s.getAsBool(borderTopKey, false), + s.getAsBool(borderRightKey, false), + s.getAsBool(borderBottomKey, false), + s.getAsBool(borderLeftKey, false) +} + +// GetBorderStyle returns the style's border style (type Border). If no value +// is set Border{} is returned. +func (s Style) GetBorderStyle() Border { + return s.getBorderStyle() +} + +// GetBorderTop returns the style's top border setting. If no value is set +// false is returned. +func (s Style) GetBorderTop() bool { + return s.getAsBool(borderTopKey, false) +} + +// GetBorderRight returns the style's right border setting. If no value is set +// false is returned. +func (s Style) GetBorderRight() bool { + return s.getAsBool(borderRightKey, false) +} + +// GetBorderBottom returns the style's bottom border setting. If no value is +// set false is returned. +func (s Style) GetBorderBottom() bool { + return s.getAsBool(borderBottomKey, false) +} + +// GetBorderLeft returns the style's left border setting. If no value is +// set false is returned. +func (s Style) GetBorderLeft() bool { + return s.getAsBool(borderLeftKey, false) +} + +// GetBorderTopForeground returns the style's border top foreground color. If +// no value is set NoColor{} is returned. +func (s Style) GetBorderTopForeground() TerminalColor { + return s.getAsColor(borderTopForegroundKey) +} + +// GetBorderRightForeground returns the style's border right foreground color. +// If no value is set NoColor{} is returned. +func (s Style) GetBorderRightForeground() TerminalColor { + return s.getAsColor(borderRightForegroundKey) +} + +// GetBorderBottomForeground returns the style's border bottom foreground +// color. If no value is set NoColor{} is returned. +func (s Style) GetBorderBottomForeground() TerminalColor { + return s.getAsColor(borderBottomForegroundKey) +} + +// GetBorderLeftForeground returns the style's border left foreground +// color. If no value is set NoColor{} is returned. +func (s Style) GetBorderLeftForeground() TerminalColor { + return s.getAsColor(borderLeftForegroundKey) +} + +// GetBorderTopBackground returns the style's border top background color. If +// no value is set NoColor{} is returned. +func (s Style) GetBorderTopBackground() TerminalColor { + return s.getAsColor(borderTopBackgroundKey) +} + +// GetBorderRightBackground returns the style's border right background color. +// If no value is set NoColor{} is returned. +func (s Style) GetBorderRightBackground() TerminalColor { + return s.getAsColor(borderRightBackgroundKey) +} + +// GetBorderBottomBackground returns the style's border bottom background +// color. If no value is set NoColor{} is returned. +func (s Style) GetBorderBottomBackground() TerminalColor { + return s.getAsColor(borderBottomBackgroundKey) +} + +// GetBorderLeftBackground returns the style's border left background +// color. If no value is set NoColor{} is returned. +func (s Style) GetBorderLeftBackground() TerminalColor { + return s.getAsColor(borderLeftBackgroundKey) +} + +// GetBorderTopWidth returns the width of the top border. If borders contain +// runes of varying widths, the widest rune is returned. If no border exists on +// the top edge, 0 is returned. +// +// Deprecated: This function simply calls Style.GetBorderTopSize. +func (s Style) GetBorderTopWidth() int { + return s.GetBorderTopSize() +} + +// GetBorderTopSize returns the width of the top border. If borders contain +// runes of varying widths, the widest rune is returned. If no border exists on +// the top edge, 0 is returned. +func (s Style) GetBorderTopSize() int { + if !s.getAsBool(borderTopKey, false) { + return 0 + } + return s.getBorderStyle().GetTopSize() +} + +// GetBorderLeftSize returns the width of the left border. If borders contain +// runes of varying widths, the widest rune is returned. If no border exists on +// the left edge, 0 is returned. +func (s Style) GetBorderLeftSize() int { + if !s.getAsBool(borderLeftKey, false) { + return 0 + } + return s.getBorderStyle().GetLeftSize() +} + +// GetBorderBottomSize returns the width of the bottom border. If borders +// contain runes of varying widths, the widest rune is returned. If no border +// exists on the left edge, 0 is returned. +func (s Style) GetBorderBottomSize() int { + if !s.getAsBool(borderBottomKey, false) { + return 0 + } + return s.getBorderStyle().GetBottomSize() +} + +// GetBorderRightSize returns the width of the right border. If borders +// contain runes of varying widths, the widest rune is returned. If no border +// exists on the right edge, 0 is returned. +func (s Style) GetBorderRightSize() int { + if !s.getAsBool(borderRightKey, false) { + return 0 + } + return s.getBorderStyle().GetRightSize() +} + +// GetHorizontalBorderSize returns the width of the horizontal borders. If +// borders contain runes of varying widths, the widest rune is returned. If no +// border exists on the horizontal edges, 0 is returned. +func (s Style) GetHorizontalBorderSize() int { + return s.GetBorderLeftSize() + s.GetBorderRightSize() +} + +// GetVerticalBorderSize returns the width of the vertical borders. If +// borders contain runes of varying widths, the widest rune is returned. If no +// border exists on the vertical edges, 0 is returned. +func (s Style) GetVerticalBorderSize() int { + return s.GetBorderTopSize() + s.GetBorderBottomSize() +} + +// GetInline returns the style's inline setting. If no value is set false is +// returned. +func (s Style) GetInline() bool { + return s.getAsBool(inlineKey, false) +} + +// GetMaxWidth returns the style's max width setting. If no value is set 0 is +// returned. +func (s Style) GetMaxWidth() int { + return s.getAsInt(maxWidthKey) +} + +// GetMaxHeight returns the style's max height setting. If no value is set 0 is +// returned. +func (s Style) GetMaxHeight() int { + return s.getAsInt(maxHeightKey) +} + +// GetTabWidth returns the style's tab width setting. If no value is set 4 is +// returned which is the implicit default. +func (s Style) GetTabWidth() int { + return s.getAsInt(tabWidthKey) +} + +// GetUnderlineSpaces returns whether or not the style is set to underline +// spaces. If not value is set false is returned. +func (s Style) GetUnderlineSpaces() bool { + return s.getAsBool(underlineSpacesKey, false) +} + +// GetStrikethroughSpaces returns whether or not the style is set to strikethrough +// spaces. If not value is set false is returned. +func (s Style) GetStrikethroughSpaces() bool { + return s.getAsBool(strikethroughSpacesKey, false) +} + +// GetHorizontalFrameSize returns the sum of the style's horizontal margins, padding +// and border widths. +// +// Provisional: this method may be renamed. +func (s Style) GetHorizontalFrameSize() int { + return s.GetHorizontalMargins() + s.GetHorizontalPadding() + s.GetHorizontalBorderSize() +} + +// GetVerticalFrameSize returns the sum of the style's vertical margins, padding +// and border widths. +// +// Provisional: this method may be renamed. +func (s Style) GetVerticalFrameSize() int { + return s.GetVerticalMargins() + s.GetVerticalPadding() + s.GetVerticalBorderSize() +} + +// GetFrameSize returns the sum of the margins, padding and border width for +// both the horizontal and vertical margins. +func (s Style) GetFrameSize() (x, y int) { + return s.GetHorizontalFrameSize(), s.GetVerticalFrameSize() +} + +// GetTransform returns the transform set on the style. If no transform is set +// nil is returned. +func (s Style) GetTransform() func(string) string { + return s.getAsTransform(transformKey) +} + +// Returns whether or not the given property is set. +func (s Style) isSet(k propKey) bool { + _, exists := s.rules[k] + return exists +} + +func (s Style) getAsBool(k propKey, defaultVal bool) bool { + v, ok := s.rules[k] + if !ok { + return defaultVal + } + if b, ok := v.(bool); ok { + return b + } + return defaultVal +} + +func (s Style) getAsColor(k propKey) TerminalColor { + v, ok := s.rules[k] + if !ok { + return noColor + } + if c, ok := v.(TerminalColor); ok { + return c + } + return noColor +} + +func (s Style) getAsInt(k propKey) int { + v, ok := s.rules[k] + if !ok { + return 0 + } + if i, ok := v.(int); ok { + return i + } + return 0 +} + +func (s Style) getAsPosition(k propKey) Position { + v, ok := s.rules[k] + if !ok { + return Position(0) + } + if p, ok := v.(Position); ok { + return p + } + return Position(0) +} + +func (s Style) getBorderStyle() Border { + v, ok := s.rules[borderStyleKey] + if !ok { + return noBorder + } + if b, ok := v.(Border); ok { + return b + } + return noBorder +} + +func (s Style) getAsTransform(k propKey) func(string) string { + v, ok := s.rules[k] + if !ok { + return nil + } + if fn, ok := v.(func(string) string); ok { + return fn + } + return nil +} + +// Split a string into lines, additionally returning the size of the widest +// line. +func getLines(s string) (lines []string, widest int) { + lines = strings.Split(s, "\n") + + for _, l := range lines { + w := ansi.StringWidth(l) + if widest < w { + widest = w + } + } + + return lines, widest +} diff --git a/vendor/github.com/charmbracelet/lipgloss/join.go b/vendor/github.com/charmbracelet/lipgloss/join.go new file mode 100644 index 000000000..8e3115b05 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/join.go @@ -0,0 +1,175 @@ +package lipgloss + +import ( + "math" + "strings" + + "github.com/charmbracelet/x/exp/term/ansi" +) + +// JoinHorizontal is a utility function for horizontally joining two +// potentially multi-lined strings along a vertical axis. The first argument is +// the position, with 0 being all the way at the top and 1 being all the way +// at the bottom. +// +// If you just want to align to the top, center or bottom you may as well just +// use the helper constants Top, Center, and Bottom. +// +// Example: +// +// blockB := "...\n...\n..." +// blockA := "...\n...\n...\n...\n..." +// +// // Join 20% from the top +// str := lipgloss.JoinHorizontal(0.2, blockA, blockB) +// +// // Join on the top edge +// str := lipgloss.JoinHorizontal(lipgloss.Top, blockA, blockB) +func JoinHorizontal(pos Position, strs ...string) string { + if len(strs) == 0 { + return "" + } + if len(strs) == 1 { + return strs[0] + } + + var ( + // Groups of strings broken into multiple lines + blocks = make([][]string, len(strs)) + + // Max line widths for the above text blocks + maxWidths = make([]int, len(strs)) + + // Height of the tallest block + maxHeight int + ) + + // Break text blocks into lines and get max widths for each text block + for i, str := range strs { + blocks[i], maxWidths[i] = getLines(str) + if len(blocks[i]) > maxHeight { + maxHeight = len(blocks[i]) + } + } + + // Add extra lines to make each side the same height + for i := range blocks { + if len(blocks[i]) >= maxHeight { + continue + } + + extraLines := make([]string, maxHeight-len(blocks[i])) + + switch pos { //nolint:exhaustive + case Top: + blocks[i] = append(blocks[i], extraLines...) + + case Bottom: + blocks[i] = append(extraLines, blocks[i]...) + + default: // Somewhere in the middle + n := len(extraLines) + split := int(math.Round(float64(n) * pos.value())) + top := n - split + bottom := n - top + + blocks[i] = append(extraLines[top:], blocks[i]...) + blocks[i] = append(blocks[i], extraLines[bottom:]...) + } + } + + // Merge lines + var b strings.Builder + for i := range blocks[0] { // remember, all blocks have the same number of members now + for j, block := range blocks { + b.WriteString(block[i]) + + // Also make lines the same length + b.WriteString(strings.Repeat(" ", maxWidths[j]-ansi.StringWidth(block[i]))) + } + if i < len(blocks[0])-1 { + b.WriteRune('\n') + } + } + + return b.String() +} + +// JoinVertical is a utility function for vertically joining two potentially +// multi-lined strings along a horizontal axis. The first argument is the +// position, with 0 being all the way to the left and 1 being all the way to +// the right. +// +// If you just want to align to the left, right or center you may as well just +// use the helper constants Left, Center, and Right. +// +// Example: +// +// blockB := "...\n...\n..." +// blockA := "...\n...\n...\n...\n..." +// +// // Join 20% from the top +// str := lipgloss.JoinVertical(0.2, blockA, blockB) +// +// // Join on the right edge +// str := lipgloss.JoinVertical(lipgloss.Right, blockA, blockB) +func JoinVertical(pos Position, strs ...string) string { + if len(strs) == 0 { + return "" + } + if len(strs) == 1 { + return strs[0] + } + + var ( + blocks = make([][]string, len(strs)) + maxWidth int + ) + + for i := range strs { + var w int + blocks[i], w = getLines(strs[i]) + if w > maxWidth { + maxWidth = w + } + } + + var b strings.Builder + for i, block := range blocks { + for j, line := range block { + w := maxWidth - ansi.StringWidth(line) + + switch pos { //nolint:exhaustive + case Left: + b.WriteString(line) + b.WriteString(strings.Repeat(" ", w)) + + case Right: + b.WriteString(strings.Repeat(" ", w)) + b.WriteString(line) + + default: // Somewhere in the middle + if w < 1 { + b.WriteString(line) + break + } + + split := int(math.Round(float64(w) * pos.value())) + right := w - split + left := w - right + + b.WriteString(strings.Repeat(" ", left)) + b.WriteString(line) + b.WriteString(strings.Repeat(" ", right)) + } + + // Write a newline as long as we're not on the last line of the + // last block. + if !(i == len(blocks)-1 && j == len(block)-1) { + b.WriteRune('\n') + } + } + } + + return b.String() +} diff --git a/vendor/github.com/charmbracelet/lipgloss/position.go b/vendor/github.com/charmbracelet/lipgloss/position.go new file mode 100644 index 000000000..f57b7bb91 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/position.go @@ -0,0 +1,154 @@ +package lipgloss + +import ( + "math" + "strings" + + "github.com/charmbracelet/x/exp/term/ansi" +) + +// Position represents a position along a horizontal or vertical axis. It's in +// situations where an axis is involved, like alignment, joining, placement and +// so on. +// +// A value of 0 represents the start (the left or top) and 1 represents the end +// (the right or bottom). 0.5 represents the center. +// +// There are constants Top, Bottom, Center, Left and Right in this package that +// can be used to aid readability. +type Position float64 + +func (p Position) value() float64 { + return math.Min(1, math.Max(0, float64(p))) +} + +// Position aliases. +const ( + Top Position = 0.0 + Bottom Position = 1.0 + Center Position = 0.5 + Left Position = 0.0 + Right Position = 1.0 +) + +// Place places a string or text block vertically in an unstyled box of a given +// width or height. +func Place(width, height int, hPos, vPos Position, str string, opts ...WhitespaceOption) string { + return renderer.Place(width, height, hPos, vPos, str, opts...) +} + +// Place places a string or text block vertically in an unstyled box of a given +// width or height. +func (r *Renderer) Place(width, height int, hPos, vPos Position, str string, opts ...WhitespaceOption) string { + return r.PlaceVertical(height, vPos, r.PlaceHorizontal(width, hPos, str, opts...), opts...) +} + +// PlaceHorizontal places a string or text block horizontally in an unstyled +// block of a given width. If the given width is shorter than the max width of +// the string (measured by its longest line) this will be a noop. +func PlaceHorizontal(width int, pos Position, str string, opts ...WhitespaceOption) string { + return renderer.PlaceHorizontal(width, pos, str, opts...) +} + +// PlaceHorizontal places a string or text block horizontally in an unstyled +// block of a given width. If the given width is shorter than the max width of +// the string (measured by its longest line) this will be a noöp. +func (r *Renderer) PlaceHorizontal(width int, pos Position, str string, opts ...WhitespaceOption) string { + lines, contentWidth := getLines(str) + gap := width - contentWidth + + if gap <= 0 { + return str + } + + ws := newWhitespace(r, opts...) + + var b strings.Builder + for i, l := range lines { + // Is this line shorter than the longest line? + short := max(0, contentWidth-ansi.StringWidth(l)) + + switch pos { //nolint:exhaustive + case Left: + b.WriteString(l) + b.WriteString(ws.render(gap + short)) + + case Right: + b.WriteString(ws.render(gap + short)) + b.WriteString(l) + + default: // somewhere in the middle + totalGap := gap + short + + split := int(math.Round(float64(totalGap) * pos.value())) + left := totalGap - split + right := totalGap - left + + b.WriteString(ws.render(left)) + b.WriteString(l) + b.WriteString(ws.render(right)) + } + + if i < len(lines)-1 { + b.WriteRune('\n') + } + } + + return b.String() +} + +// PlaceVertical places a string or text block vertically in an unstyled block +// of a given height. If the given height is shorter than the height of the +// string (measured by its newlines) then this will be a noop. +func PlaceVertical(height int, pos Position, str string, opts ...WhitespaceOption) string { + return renderer.PlaceVertical(height, pos, str, opts...) +} + +// PlaceVertical places a string or text block vertically in an unstyled block +// of a given height. If the given height is shorter than the height of the +// string (measured by its newlines) then this will be a noöp. +func (r *Renderer) PlaceVertical(height int, pos Position, str string, opts ...WhitespaceOption) string { + contentHeight := strings.Count(str, "\n") + 1 + gap := height - contentHeight + + if gap <= 0 { + return str + } + + ws := newWhitespace(r, opts...) + + _, width := getLines(str) + emptyLine := ws.render(width) + b := strings.Builder{} + + switch pos { //nolint:exhaustive + case Top: + b.WriteString(str) + b.WriteRune('\n') + for i := 0; i < gap; i++ { + b.WriteString(emptyLine) + if i < gap-1 { + b.WriteRune('\n') + } + } + + case Bottom: + b.WriteString(strings.Repeat(emptyLine+"\n", gap)) + b.WriteString(str) + + default: // Somewhere in the middle + split := int(math.Round(float64(gap) * pos.value())) + top := gap - split + bottom := gap - top + + b.WriteString(strings.Repeat(emptyLine+"\n", top)) + b.WriteString(str) + + for i := 0; i < bottom; i++ { + b.WriteRune('\n') + b.WriteString(emptyLine) + } + } + + return b.String() +} diff --git a/vendor/github.com/charmbracelet/lipgloss/renderer.go b/vendor/github.com/charmbracelet/lipgloss/renderer.go new file mode 100644 index 000000000..85ffd2545 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/renderer.go @@ -0,0 +1,184 @@ +package lipgloss + +import ( + "io" + "sync" + + "github.com/muesli/termenv" +) + +// We're manually creating the struct here to avoid initializing the output and +// query the terminal multiple times. +var renderer = &Renderer{ + output: termenv.DefaultOutput(), +} + +// Renderer is a lipgloss terminal renderer. +type Renderer struct { + output *termenv.Output + colorProfile termenv.Profile + hasDarkBackground bool + + getColorProfile sync.Once + explicitColorProfile bool + + getBackgroundColor sync.Once + explicitBackgroundColor bool + + mtx sync.RWMutex +} + +// RendererOption is a function that can be used to configure a [Renderer]. +type RendererOption func(r *Renderer) + +// DefaultRenderer returns the default renderer. +func DefaultRenderer() *Renderer { + return renderer +} + +// SetDefaultRenderer sets the default global renderer. +func SetDefaultRenderer(r *Renderer) { + renderer = r +} + +// NewRenderer creates a new Renderer. +// +// w will be used to determine the terminal's color capabilities. +func NewRenderer(w io.Writer, opts ...termenv.OutputOption) *Renderer { + r := &Renderer{ + output: termenv.NewOutput(w, opts...), + } + return r +} + +// Output returns the termenv output. +func (r *Renderer) Output() *termenv.Output { + r.mtx.RLock() + defer r.mtx.RUnlock() + return r.output +} + +// SetOutput sets the termenv output. +func (r *Renderer) SetOutput(o *termenv.Output) { + r.mtx.Lock() + defer r.mtx.Unlock() + r.output = o +} + +// ColorProfile returns the detected termenv color profile. +func (r *Renderer) ColorProfile() termenv.Profile { + r.mtx.RLock() + defer r.mtx.RUnlock() + + if !r.explicitColorProfile { + r.getColorProfile.Do(func() { + // NOTE: we don't need to lock here because sync.Once provides its + // own locking mechanism. + r.colorProfile = r.output.EnvColorProfile() + }) + } + + return r.colorProfile +} + +// ColorProfile returns the detected termenv color profile. +func ColorProfile() termenv.Profile { + return renderer.ColorProfile() +} + +// SetColorProfile sets the color profile on the renderer. This function exists +// mostly for testing purposes so that you can assure you're testing against +// a specific profile. +// +// Outside of testing you likely won't want to use this function as the color +// profile will detect and cache the terminal's color capabilities and choose +// the best available profile. +// +// Available color profiles are: +// +// termenv.Ascii // no color, 1-bit +// termenv.ANSI //16 colors, 4-bit +// termenv.ANSI256 // 256 colors, 8-bit +// termenv.TrueColor // 16,777,216 colors, 24-bit +// +// This function is thread-safe. +func (r *Renderer) SetColorProfile(p termenv.Profile) { + r.mtx.Lock() + defer r.mtx.Unlock() + + r.colorProfile = p + r.explicitColorProfile = true +} + +// SetColorProfile sets the color profile on the default renderer. This +// function exists mostly for testing purposes so that you can assure you're +// testing against a specific profile. +// +// Outside of testing you likely won't want to use this function as the color +// profile will detect and cache the terminal's color capabilities and choose +// the best available profile. +// +// Available color profiles are: +// +// termenv.Ascii // no color, 1-bit +// termenv.ANSI //16 colors, 4-bit +// termenv.ANSI256 // 256 colors, 8-bit +// termenv.TrueColor // 16,777,216 colors, 24-bit +// +// This function is thread-safe. +func SetColorProfile(p termenv.Profile) { + renderer.SetColorProfile(p) +} + +// HasDarkBackground returns whether or not the terminal has a dark background. +func HasDarkBackground() bool { + return renderer.HasDarkBackground() +} + +// HasDarkBackground returns whether or not the renderer will render to a dark +// background. A dark background can either be auto-detected, or set explicitly +// on the renderer. +func (r *Renderer) HasDarkBackground() bool { + r.mtx.RLock() + defer r.mtx.RUnlock() + + if !r.explicitBackgroundColor { + r.getBackgroundColor.Do(func() { + // NOTE: we don't need to lock here because sync.Once provides its + // own locking mechanism. + r.hasDarkBackground = r.output.HasDarkBackground() + }) + } + + return r.hasDarkBackground +} + +// SetHasDarkBackground sets the background color detection value for the +// default renderer. This function exists mostly for testing purposes so that +// you can assure you're testing against a specific background color setting. +// +// Outside of testing you likely won't want to use this function as the +// backgrounds value will be automatically detected and cached against the +// terminal's current background color setting. +// +// This function is thread-safe. +func SetHasDarkBackground(b bool) { + renderer.SetHasDarkBackground(b) +} + +// SetHasDarkBackground sets the background color detection value on the +// renderer. This function exists mostly for testing purposes so that you can +// assure you're testing against a specific background color setting. +// +// Outside of testing you likely won't want to use this function as the +// backgrounds value will be automatically detected and cached against the +// terminal's current background color setting. +// +// This function is thread-safe. +func (r *Renderer) SetHasDarkBackground(b bool) { + r.mtx.Lock() + defer r.mtx.Unlock() + + r.hasDarkBackground = b + r.explicitBackgroundColor = true +} diff --git a/vendor/github.com/charmbracelet/lipgloss/runes.go b/vendor/github.com/charmbracelet/lipgloss/runes.go new file mode 100644 index 000000000..7a49e326c --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/runes.go @@ -0,0 +1,43 @@ +package lipgloss + +import ( + "strings" +) + +// StyleRunes apply a given style to runes at the given indices in the string. +// Note that you must provide styling options for both matched and unmatched +// runes. Indices out of bounds will be ignored. +func StyleRunes(str string, indices []int, matched, unmatched Style) string { + // Convert slice of indices to a map for easier lookups + m := make(map[int]struct{}) + for _, i := range indices { + m[i] = struct{}{} + } + + var ( + out strings.Builder + group strings.Builder + style Style + runes = []rune(str) + ) + + for i, r := range runes { + group.WriteRune(r) + + _, matches := m[i] + _, nextMatches := m[i+1] + + if matches != nextMatches || i == len(runes)-1 { + // Flush + if matches { + style = matched + } else { + style = unmatched + } + out.WriteString(style.Render(group.String())) + group.Reset() + } + } + + return out.String() +} diff --git a/vendor/github.com/charmbracelet/lipgloss/set.go b/vendor/github.com/charmbracelet/lipgloss/set.go new file mode 100644 index 000000000..5846b598a --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/set.go @@ -0,0 +1,670 @@ +package lipgloss + +// This could (should) probably just be moved into NewStyle(). We've broken it +// out, so we can call it in a lazy way. +func (s *Style) init() { + if s.rules == nil { + s.rules = make(rules) + } +} + +// Set a value on the underlying rules map. +func (s *Style) set(key propKey, value interface{}) { + s.init() + + switch v := value.(type) { + case int: + // TabWidth is the only property that may have a negative value (and + // that negative value can be no less than -1). + if key == tabWidthKey { + s.rules[key] = v + break + } + + // We don't allow negative integers on any of our other values, so just keep + // them at zero or above. We could use uints instead, but the + // conversions are a little tedious, so we're sticking with ints for + // sake of usability. + s.rules[key] = max(0, v) + default: + s.rules[key] = v + } +} + +// Bold sets a bold formatting rule. +func (s Style) Bold(v bool) Style { + s.set(boldKey, v) + return s +} + +// Italic sets an italic formatting rule. In some terminal emulators this will +// render with "reverse" coloring if not italic font variant is available. +func (s Style) Italic(v bool) Style { + s.set(italicKey, v) + return s +} + +// Underline sets an underline rule. By default, underlines will not be drawn on +// whitespace like margins and padding. To change this behavior set +// UnderlineSpaces. +func (s Style) Underline(v bool) Style { + s.set(underlineKey, v) + return s +} + +// Strikethrough sets a strikethrough rule. By default, strikes will not be +// drawn on whitespace like margins and padding. To change this behavior set +// StrikethroughSpaces. +func (s Style) Strikethrough(v bool) Style { + s.set(strikethroughKey, v) + return s +} + +// Reverse sets a rule for inverting foreground and background colors. +func (s Style) Reverse(v bool) Style { + s.set(reverseKey, v) + return s +} + +// Blink sets a rule for blinking foreground text. +func (s Style) Blink(v bool) Style { + s.set(blinkKey, v) + return s +} + +// Faint sets a rule for rendering the foreground color in a dimmer shade. +func (s Style) Faint(v bool) Style { + s.set(faintKey, v) + return s +} + +// Foreground sets a foreground color. +// +// // Sets the foreground to blue +// s := lipgloss.NewStyle().Foreground(lipgloss.Color("#0000ff")) +// +// // Removes the foreground color +// s.Foreground(lipgloss.NoColor) +func (s Style) Foreground(c TerminalColor) Style { + s.set(foregroundKey, c) + return s +} + +// Background sets a background color. +func (s Style) Background(c TerminalColor) Style { + s.set(backgroundKey, c) + return s +} + +// Width sets the width of the block before applying margins. The width, if +// set, also determines where text will wrap. +func (s Style) Width(i int) Style { + s.set(widthKey, i) + return s +} + +// Height sets the height of the block before applying margins. If the height of +// the text block is less than this value after applying padding (or not), the +// block will be set to this height. +func (s Style) Height(i int) Style { + s.set(heightKey, i) + return s +} + +// Align is a shorthand method for setting horizontal and vertical alignment. +// +// With one argument, the position value is applied to the horizontal alignment. +// +// With two arguments, the value is applied to the horizontal and vertical +// alignments, in that order. +func (s Style) Align(p ...Position) Style { + if len(p) > 0 { + s.set(alignHorizontalKey, p[0]) + } + if len(p) > 1 { + s.set(alignVerticalKey, p[1]) + } + return s +} + +// AlignHorizontal sets a horizontal text alignment rule. +func (s Style) AlignHorizontal(p Position) Style { + s.set(alignHorizontalKey, p) + return s +} + +// AlignVertical sets a vertical text alignment rule. +func (s Style) AlignVertical(p Position) Style { + s.set(alignVerticalKey, p) + return s +} + +// Padding is a shorthand method for setting padding on all sides at once. +// +// With one argument, the value is applied to all sides. +// +// With two arguments, the value is applied to the vertical and horizontal +// sides, in that order. +// +// With three arguments, the value is applied to the top side, the horizontal +// sides, and the bottom side, in that order. +// +// With four arguments, the value is applied clockwise starting from the top +// side, followed by the right side, then the bottom, and finally the left. +// +// With more than four arguments no padding will be added. +func (s Style) Padding(i ...int) Style { + top, right, bottom, left, ok := whichSidesInt(i...) + if !ok { + return s + } + + s.set(paddingTopKey, top) + s.set(paddingRightKey, right) + s.set(paddingBottomKey, bottom) + s.set(paddingLeftKey, left) + return s +} + +// PaddingLeft adds padding on the left. +func (s Style) PaddingLeft(i int) Style { + s.set(paddingLeftKey, i) + return s +} + +// PaddingRight adds padding on the right. +func (s Style) PaddingRight(i int) Style { + s.set(paddingRightKey, i) + return s +} + +// PaddingTop adds padding to the top of the block. +func (s Style) PaddingTop(i int) Style { + s.set(paddingTopKey, i) + return s +} + +// PaddingBottom adds padding to the bottom of the block. +func (s Style) PaddingBottom(i int) Style { + s.set(paddingBottomKey, i) + return s +} + +// ColorWhitespace determines whether or not the background color should be +// applied to the padding. This is true by default as it's more than likely the +// desired and expected behavior, but it can be disabled for certain graphic +// effects. +func (s Style) ColorWhitespace(v bool) Style { + s.set(colorWhitespaceKey, v) + return s +} + +// Margin is a shorthand method for setting margins on all sides at once. +// +// With one argument, the value is applied to all sides. +// +// With two arguments, the value is applied to the vertical and horizontal +// sides, in that order. +// +// With three arguments, the value is applied to the top side, the horizontal +// sides, and the bottom side, in that order. +// +// With four arguments, the value is applied clockwise starting from the top +// side, followed by the right side, then the bottom, and finally the left. +// +// With more than four arguments no margin will be added. +func (s Style) Margin(i ...int) Style { + top, right, bottom, left, ok := whichSidesInt(i...) + if !ok { + return s + } + + s.set(marginTopKey, top) + s.set(marginRightKey, right) + s.set(marginBottomKey, bottom) + s.set(marginLeftKey, left) + return s +} + +// MarginLeft sets the value of the left margin. +func (s Style) MarginLeft(i int) Style { + s.set(marginLeftKey, i) + return s +} + +// MarginRight sets the value of the right margin. +func (s Style) MarginRight(i int) Style { + s.set(marginRightKey, i) + return s +} + +// MarginTop sets the value of the top margin. +func (s Style) MarginTop(i int) Style { + s.set(marginTopKey, i) + return s +} + +// MarginBottom sets the value of the bottom margin. +func (s Style) MarginBottom(i int) Style { + s.set(marginBottomKey, i) + return s +} + +// MarginBackground sets the background color of the margin. Note that this is +// also set when inheriting from a style with a background color. In that case +// the background color on that style will set the margin color on this style. +func (s Style) MarginBackground(c TerminalColor) Style { + s.set(marginBackgroundKey, c) + return s +} + +// Border is shorthand for setting the border style and which sides should +// have a border at once. The variadic argument sides works as follows: +// +// With one value, the value is applied to all sides. +// +// With two values, the values are applied to the vertical and horizontal +// sides, in that order. +// +// With three values, the values are applied to the top side, the horizontal +// sides, and the bottom side, in that order. +// +// With four values, the values are applied clockwise starting from the top +// side, followed by the right side, then the bottom, and finally the left. +// +// With more than four arguments the border will be applied to all sides. +// +// Examples: +// +// // Applies borders to the top and bottom only +// lipgloss.NewStyle().Border(lipgloss.NormalBorder(), true, false) +// +// // Applies rounded borders to the right and bottom only +// lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), false, true, true, false) +func (s Style) Border(b Border, sides ...bool) Style { + s.set(borderStyleKey, b) + + top, right, bottom, left, ok := whichSidesBool(sides...) + if !ok { + top = true + right = true + bottom = true + left = true + } + + s.set(borderTopKey, top) + s.set(borderRightKey, right) + s.set(borderBottomKey, bottom) + s.set(borderLeftKey, left) + + return s +} + +// BorderStyle defines the Border on a style. A Border contains a series of +// definitions for the sides and corners of a border. +// +// Note that if border visibility has not been set for any sides when setting +// the border style, the border will be enabled for all sides during rendering. +// +// You can define border characters as you'd like, though several default +// styles are included: NormalBorder(), RoundedBorder(), BlockBorder(), +// OuterHalfBlockBorder(), InnerHalfBlockBorder(), ThickBorder(), +// and DoubleBorder(). +// +// Example: +// +// lipgloss.NewStyle().BorderStyle(lipgloss.ThickBorder()) +func (s Style) BorderStyle(b Border) Style { + s.set(borderStyleKey, b) + return s +} + +// BorderTop determines whether or not to draw a top border. +func (s Style) BorderTop(v bool) Style { + s.set(borderTopKey, v) + return s +} + +// BorderRight determines whether or not to draw a right border. +func (s Style) BorderRight(v bool) Style { + s.set(borderRightKey, v) + return s +} + +// BorderBottom determines whether or not to draw a bottom border. +func (s Style) BorderBottom(v bool) Style { + s.set(borderBottomKey, v) + return s +} + +// BorderLeft determines whether or not to draw a left border. +func (s Style) BorderLeft(v bool) Style { + s.set(borderLeftKey, v) + return s +} + +// BorderForeground is a shorthand function for setting all of the +// foreground colors of the borders at once. The arguments work as follows: +// +// With one argument, the argument is applied to all sides. +// +// With two arguments, the arguments are applied to the vertical and horizontal +// sides, in that order. +// +// With three arguments, the arguments are applied to the top side, the +// horizontal sides, and the bottom side, in that order. +// +// With four arguments, the arguments are applied clockwise starting from the +// top side, followed by the right side, then the bottom, and finally the left. +// +// With more than four arguments nothing will be set. +func (s Style) BorderForeground(c ...TerminalColor) Style { + if len(c) == 0 { + return s + } + + top, right, bottom, left, ok := whichSidesColor(c...) + if !ok { + return s + } + + s.set(borderTopForegroundKey, top) + s.set(borderRightForegroundKey, right) + s.set(borderBottomForegroundKey, bottom) + s.set(borderLeftForegroundKey, left) + + return s +} + +// BorderTopForeground set the foreground color for the top of the border. +func (s Style) BorderTopForeground(c TerminalColor) Style { + s.set(borderTopForegroundKey, c) + return s +} + +// BorderRightForeground sets the foreground color for the right side of the +// border. +func (s Style) BorderRightForeground(c TerminalColor) Style { + s.set(borderRightForegroundKey, c) + return s +} + +// BorderBottomForeground sets the foreground color for the bottom of the +// border. +func (s Style) BorderBottomForeground(c TerminalColor) Style { + s.set(borderBottomForegroundKey, c) + return s +} + +// BorderLeftForeground sets the foreground color for the left side of the +// border. +func (s Style) BorderLeftForeground(c TerminalColor) Style { + s.set(borderLeftForegroundKey, c) + return s +} + +// BorderBackground is a shorthand function for setting all of the +// background colors of the borders at once. The arguments work as follows: +// +// With one argument, the argument is applied to all sides. +// +// With two arguments, the arguments are applied to the vertical and horizontal +// sides, in that order. +// +// With three arguments, the arguments are applied to the top side, the +// horizontal sides, and the bottom side, in that order. +// +// With four arguments, the arguments are applied clockwise starting from the +// top side, followed by the right side, then the bottom, and finally the left. +// +// With more than four arguments nothing will be set. +func (s Style) BorderBackground(c ...TerminalColor) Style { + if len(c) == 0 { + return s + } + + top, right, bottom, left, ok := whichSidesColor(c...) + if !ok { + return s + } + + s.set(borderTopBackgroundKey, top) + s.set(borderRightBackgroundKey, right) + s.set(borderBottomBackgroundKey, bottom) + s.set(borderLeftBackgroundKey, left) + + return s +} + +// BorderTopBackground sets the background color of the top of the border. +func (s Style) BorderTopBackground(c TerminalColor) Style { + s.set(borderTopBackgroundKey, c) + return s +} + +// BorderRightBackground sets the background color of right side the border. +func (s Style) BorderRightBackground(c TerminalColor) Style { + s.set(borderRightBackgroundKey, c) + return s +} + +// BorderBottomBackground sets the background color of the bottom of the +// border. +func (s Style) BorderBottomBackground(c TerminalColor) Style { + s.set(borderBottomBackgroundKey, c) + return s +} + +// BorderLeftBackground set the background color of the left side of the +// border. +func (s Style) BorderLeftBackground(c TerminalColor) Style { + s.set(borderLeftBackgroundKey, c) + return s +} + +// Inline makes rendering output one line and disables the rendering of +// margins, padding and borders. This is useful when you need a style to apply +// only to font rendering and don't want it to change any physical dimensions. +// It works well with Style.MaxWidth. +// +// Because this in intended to be used at the time of render, this method will +// not mutate the style and instead return a copy. +// +// Example: +// +// var userInput string = "..." +// var userStyle = text.Style{ /* ... */ } +// fmt.Println(userStyle.Inline(true).Render(userInput)) +func (s Style) Inline(v bool) Style { + o := s.Copy() + o.set(inlineKey, v) + return o +} + +// MaxWidth applies a max width to a given style. This is useful in enforcing +// a certain width at render time, particularly with arbitrary strings and +// styles. +// +// Because this in intended to be used at the time of render, this method will +// not mutate the style and instead return a copy. +// +// Example: +// +// var userInput string = "..." +// var userStyle = text.Style{ /* ... */ } +// fmt.Println(userStyle.MaxWidth(16).Render(userInput)) +func (s Style) MaxWidth(n int) Style { + o := s.Copy() + o.set(maxWidthKey, n) + return o +} + +// MaxHeight applies a max height to a given style. This is useful in enforcing +// a certain height at render time, particularly with arbitrary strings and +// styles. +// +// Because this in intended to be used at the time of render, this method will +// not mutate the style and instead returns a copy. +func (s Style) MaxHeight(n int) Style { + o := s.Copy() + o.set(maxHeightKey, n) + return o +} + +// NoTabConversion can be passed to [Style.TabWidth] to disable the replacement +// of tabs with spaces at render time. +const NoTabConversion = -1 + +// TabWidth sets the number of spaces that a tab (/t) should be rendered as. +// When set to 0, tabs will be removed. To disable the replacement of tabs with +// spaces entirely, set this to [NoTabConversion]. +// +// By default, tabs will be replaced with 4 spaces. +func (s Style) TabWidth(n int) Style { + if n <= -1 { + n = -1 + } + s.set(tabWidthKey, n) + return s +} + +// UnderlineSpaces determines whether to underline spaces between words. By +// default, this is true. Spaces can also be underlined without underlining the +// text itself. +func (s Style) UnderlineSpaces(v bool) Style { + s.set(underlineSpacesKey, v) + return s +} + +// StrikethroughSpaces determines whether to apply strikethroughs to spaces +// between words. By default, this is true. Spaces can also be struck without +// underlining the text itself. +func (s Style) StrikethroughSpaces(v bool) Style { + s.set(strikethroughSpacesKey, v) + return s +} + +// Transform applies a given function to a string at render time, allowing for +// the string being rendered to be manipuated. +// +// Example: +// +// s := NewStyle().Transform(strings.ToUpper) +// fmt.Println(s.Render("raow!") // "RAOW!" +func (s Style) Transform(fn func(string) string) Style { + s.set(transformKey, fn) + return s +} + +// Renderer sets the renderer for the style. This is useful for changing the +// renderer for a style that is being used in a different context. +func (s Style) Renderer(r *Renderer) Style { + s.r = r + return s +} + +// whichSidesInt is a helper method for setting values on sides of a block based +// on the number of arguments. It follows the CSS shorthand rules for blocks +// like margin, padding. and borders. Here are how the rules work: +// +// 0 args: do nothing +// 1 arg: all sides +// 2 args: top -> bottom +// 3 args: top -> horizontal -> bottom +// 4 args: top -> right -> bottom -> left +// 5+ args: do nothing. +func whichSidesInt(i ...int) (top, right, bottom, left int, ok bool) { + switch len(i) { + case 1: + top = i[0] + bottom = i[0] + left = i[0] + right = i[0] + ok = true + case 2: //nolint:gomnd + top = i[0] + bottom = i[0] + left = i[1] + right = i[1] + ok = true + case 3: //nolint:gomnd + top = i[0] + left = i[1] + right = i[1] + bottom = i[2] + ok = true + case 4: //nolint:gomnd + top = i[0] + right = i[1] + bottom = i[2] + left = i[3] + ok = true + } + return top, right, bottom, left, ok +} + +// whichSidesBool is like whichSidesInt, except it operates on a series of +// boolean values. See the comment on whichSidesInt for details on how this +// works. +func whichSidesBool(i ...bool) (top, right, bottom, left bool, ok bool) { + switch len(i) { + case 1: + top = i[0] + bottom = i[0] + left = i[0] + right = i[0] + ok = true + case 2: //nolint:gomnd + top = i[0] + bottom = i[0] + left = i[1] + right = i[1] + ok = true + case 3: //nolint:gomnd + top = i[0] + left = i[1] + right = i[1] + bottom = i[2] + ok = true + case 4: //nolint:gomnd + top = i[0] + right = i[1] + bottom = i[2] + left = i[3] + ok = true + } + return top, right, bottom, left, ok +} + +// whichSidesColor is like whichSides, except it operates on a series of +// boolean values. See the comment on whichSidesInt for details on how this +// works. +func whichSidesColor(i ...TerminalColor) (top, right, bottom, left TerminalColor, ok bool) { + switch len(i) { + case 1: + top = i[0] + bottom = i[0] + left = i[0] + right = i[0] + ok = true + case 2: //nolint:gomnd + top = i[0] + bottom = i[0] + left = i[1] + right = i[1] + ok = true + case 3: //nolint:gomnd + top = i[0] + left = i[1] + right = i[1] + bottom = i[2] + ok = true + case 4: //nolint:gomnd + top = i[0] + right = i[1] + bottom = i[2] + left = i[3] + ok = true + } + return top, right, bottom, left, ok +} diff --git a/vendor/github.com/charmbracelet/lipgloss/size.go b/vendor/github.com/charmbracelet/lipgloss/size.go new file mode 100644 index 000000000..0be4d13a7 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/size.go @@ -0,0 +1,41 @@ +package lipgloss + +import ( + "strings" + + "github.com/charmbracelet/x/exp/term/ansi" +) + +// Width returns the cell width of characters in the string. ANSI sequences are +// ignored and characters wider than one cell (such as Chinese characters and +// emojis) are appropriately measured. +// +// You should use this instead of len(string) len([]rune(string) as neither +// will give you accurate results. +func Width(str string) (width int) { + for _, l := range strings.Split(str, "\n") { + w := ansi.StringWidth(l) + if w > width { + width = w + } + } + + return width +} + +// Height returns height of a string in cells. This is done simply by +// counting \n characters. If your strings use \r\n for newlines you should +// convert them to \n first, or simply write a separate function for measuring +// height. +func Height(str string) int { + return strings.Count(str, "\n") + 1 +} + +// Size returns the width and height of the string in cells. ANSI sequences are +// ignored and characters wider than one cell (such as Chinese characters and +// emojis) are appropriately measured. +func Size(str string) (width, height int) { + width = Width(str) + height = Height(str) + return width, height +} diff --git a/vendor/github.com/charmbracelet/lipgloss/style.go b/vendor/github.com/charmbracelet/lipgloss/style.go new file mode 100644 index 000000000..98540ad78 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/style.go @@ -0,0 +1,531 @@ +package lipgloss + +import ( + "strings" + "unicode" + + "github.com/charmbracelet/x/exp/term/ansi" + "github.com/muesli/termenv" +) + +const tabWidthDefault = 4 + +// Property for a key. +type propKey int + +// Available properties. +const ( + boldKey propKey = iota + italicKey + underlineKey + strikethroughKey + reverseKey + blinkKey + faintKey + foregroundKey + backgroundKey + widthKey + heightKey + alignHorizontalKey + alignVerticalKey + + // Padding. + paddingTopKey + paddingRightKey + paddingBottomKey + paddingLeftKey + + colorWhitespaceKey + + // Margins. + marginTopKey + marginRightKey + marginBottomKey + marginLeftKey + marginBackgroundKey + + // Border runes. + borderStyleKey + + // Border edges. + borderTopKey + borderRightKey + borderBottomKey + borderLeftKey + + // Border foreground colors. + borderTopForegroundKey + borderRightForegroundKey + borderBottomForegroundKey + borderLeftForegroundKey + + // Border background colors. + borderTopBackgroundKey + borderRightBackgroundKey + borderBottomBackgroundKey + borderLeftBackgroundKey + + inlineKey + maxWidthKey + maxHeightKey + tabWidthKey + underlineSpacesKey + strikethroughSpacesKey + + transformKey +) + +// A set of properties. +type rules map[propKey]interface{} + +// NewStyle returns a new, empty Style. While it's syntactic sugar for the +// Style{} primitive, it's recommended to use this function for creating styles +// in case the underlying implementation changes. It takes an optional string +// value to be set as the underlying string value for this style. +func NewStyle() Style { + return renderer.NewStyle() +} + +// NewStyle returns a new, empty Style. While it's syntactic sugar for the +// Style{} primitive, it's recommended to use this function for creating styles +// in case the underlying implementation changes. It takes an optional string +// value to be set as the underlying string value for this style. +func (r *Renderer) NewStyle() Style { + s := Style{r: r} + return s +} + +// Style contains a set of rules that comprise a style as a whole. +type Style struct { + r *Renderer + rules map[propKey]interface{} + value string +} + +// joinString joins a list of strings into a single string separated with a +// space. +func joinString(strs ...string) string { + return strings.Join(strs, " ") +} + +// SetString sets the underlying string value for this style. To render once +// the underlying string is set, use the Style.String. This method is +// a convenience for cases when having a stringer implementation is handy, such +// as when using fmt.Sprintf. You can also simply define a style and render out +// strings directly with Style.Render. +func (s Style) SetString(strs ...string) Style { + s.value = joinString(strs...) + return s +} + +// Value returns the raw, unformatted, underlying string value for this style. +func (s Style) Value() string { + return s.value +} + +// String implements stringer for a Style, returning the rendered result based +// on the rules in this style. An underlying string value must be set with +// Style.SetString prior to using this method. +func (s Style) String() string { + return s.Render() +} + +// Copy returns a copy of this style, including any underlying string values. +func (s Style) Copy() Style { + o := NewStyle() + o.init() + for k, v := range s.rules { + o.rules[k] = v + } + o.r = s.r + o.value = s.value + return o +} + +// Inherit overlays the style in the argument onto this style by copying each explicitly +// set value from the argument style onto this style if it is not already explicitly set. +// Existing set values are kept intact and not overwritten. +// +// Margins, padding, and underlying string values are not inherited. +func (s Style) Inherit(i Style) Style { + s.init() + + for k, v := range i.rules { + switch k { //nolint:exhaustive + case marginTopKey, marginRightKey, marginBottomKey, marginLeftKey: + // Margins are not inherited + continue + case paddingTopKey, paddingRightKey, paddingBottomKey, paddingLeftKey: + // Padding is not inherited + continue + case backgroundKey: + // The margins also inherit the background color + if !s.isSet(marginBackgroundKey) && !i.isSet(marginBackgroundKey) { + s.rules[marginBackgroundKey] = v + } + } + + if _, exists := s.rules[k]; exists { + continue + } + s.rules[k] = v + } + return s +} + +// Render applies the defined style formatting to a given string. +func (s Style) Render(strs ...string) string { + if s.r == nil { + s.r = renderer + } + if s.value != "" { + strs = append([]string{s.value}, strs...) + } + + var ( + str = joinString(strs...) + + p = s.r.ColorProfile() + te = p.String() + teSpace = p.String() + teWhitespace = p.String() + + bold = s.getAsBool(boldKey, false) + italic = s.getAsBool(italicKey, false) + underline = s.getAsBool(underlineKey, false) + strikethrough = s.getAsBool(strikethroughKey, false) + reverse = s.getAsBool(reverseKey, false) + blink = s.getAsBool(blinkKey, false) + faint = s.getAsBool(faintKey, false) + + fg = s.getAsColor(foregroundKey) + bg = s.getAsColor(backgroundKey) + + width = s.getAsInt(widthKey) + height = s.getAsInt(heightKey) + horizontalAlign = s.getAsPosition(alignHorizontalKey) + verticalAlign = s.getAsPosition(alignVerticalKey) + + topPadding = s.getAsInt(paddingTopKey) + rightPadding = s.getAsInt(paddingRightKey) + bottomPadding = s.getAsInt(paddingBottomKey) + leftPadding = s.getAsInt(paddingLeftKey) + + colorWhitespace = s.getAsBool(colorWhitespaceKey, true) + inline = s.getAsBool(inlineKey, false) + maxWidth = s.getAsInt(maxWidthKey) + maxHeight = s.getAsInt(maxHeightKey) + + underlineSpaces = underline && s.getAsBool(underlineSpacesKey, true) + strikethroughSpaces = strikethrough && s.getAsBool(strikethroughSpacesKey, true) + + // Do we need to style whitespace (padding and space outside + // paragraphs) separately? + styleWhitespace = reverse + + // Do we need to style spaces separately? + useSpaceStyler = underlineSpaces || strikethroughSpaces + + transform = s.getAsTransform(transformKey) + ) + + if transform != nil { + str = transform(str) + } + + if len(s.rules) == 0 { + return s.maybeConvertTabs(str) + } + + // Enable support for ANSI on the legacy Windows cmd.exe console. This is a + // no-op on non-Windows systems and on Windows runs only once. + enableLegacyWindowsANSI() + + if bold { + te = te.Bold() + } + if italic { + te = te.Italic() + } + if underline { + te = te.Underline() + } + if reverse { + if reverse { + teWhitespace = teWhitespace.Reverse() + } + te = te.Reverse() + } + if blink { + te = te.Blink() + } + if faint { + te = te.Faint() + } + + if fg != noColor { + te = te.Foreground(fg.color(s.r)) + if styleWhitespace { + teWhitespace = teWhitespace.Foreground(fg.color(s.r)) + } + if useSpaceStyler { + teSpace = teSpace.Foreground(fg.color(s.r)) + } + } + + if bg != noColor { + te = te.Background(bg.color(s.r)) + if colorWhitespace { + teWhitespace = teWhitespace.Background(bg.color(s.r)) + } + if useSpaceStyler { + teSpace = teSpace.Background(bg.color(s.r)) + } + } + + if underline { + te = te.Underline() + } + if strikethrough { + te = te.CrossOut() + } + + if underlineSpaces { + teSpace = teSpace.Underline() + } + if strikethroughSpaces { + teSpace = teSpace.CrossOut() + } + + // Potentially convert tabs to spaces + str = s.maybeConvertTabs(str) + + // Strip newlines in single line mode + if inline { + str = strings.ReplaceAll(str, "\n", "") + } + + // Word wrap + if !inline && width > 0 { + wrapAt := width - leftPadding - rightPadding + str = ansi.Wrap(str, wrapAt, "") + } + + // Render core text + { + var b strings.Builder + + l := strings.Split(str, "\n") + for i := range l { + if useSpaceStyler { + // Look for spaces and apply a different styler + for _, r := range l[i] { + if unicode.IsSpace(r) { + b.WriteString(teSpace.Styled(string(r))) + continue + } + b.WriteString(te.Styled(string(r))) + } + } else { + b.WriteString(te.Styled(l[i])) + } + if i != len(l)-1 { + b.WriteRune('\n') + } + } + + str = b.String() + } + + // Padding + if !inline { + if leftPadding > 0 { + var st *termenv.Style + if colorWhitespace || styleWhitespace { + st = &teWhitespace + } + str = padLeft(str, leftPadding, st) + } + + if rightPadding > 0 { + var st *termenv.Style + if colorWhitespace || styleWhitespace { + st = &teWhitespace + } + str = padRight(str, rightPadding, st) + } + + if topPadding > 0 { + str = strings.Repeat("\n", topPadding) + str + } + + if bottomPadding > 0 { + str += strings.Repeat("\n", bottomPadding) + } + } + + // Height + if height > 0 { + str = alignTextVertical(str, verticalAlign, height, nil) + } + + // Set alignment. This will also pad short lines with spaces so that all + // lines are the same length, so we run it under a few different conditions + // beyond alignment. + { + numLines := strings.Count(str, "\n") + + if !(numLines == 0 && width == 0) { + var st *termenv.Style + if colorWhitespace || styleWhitespace { + st = &teWhitespace + } + str = alignTextHorizontal(str, horizontalAlign, width, st) + } + } + + if !inline { + str = s.applyBorder(str) + str = s.applyMargins(str, inline) + } + + // Truncate according to MaxWidth + if maxWidth > 0 { + lines := strings.Split(str, "\n") + + for i := range lines { + lines[i] = ansi.Truncate(lines[i], maxWidth, "") + } + + str = strings.Join(lines, "\n") + } + + // Truncate according to MaxHeight + if maxHeight > 0 { + lines := strings.Split(str, "\n") + height := min(maxHeight, len(lines)) + if len(lines) > 0 { + str = strings.Join(lines[:height], "\n") + } + } + + return str +} + +func (s Style) maybeConvertTabs(str string) string { + tw := tabWidthDefault + if s.isSet(tabWidthKey) { + tw = s.getAsInt(tabWidthKey) + } + switch tw { + case -1: + return str + case 0: + return strings.ReplaceAll(str, "\t", "") + default: + return strings.ReplaceAll(str, "\t", strings.Repeat(" ", tw)) + } +} + +func (s Style) applyMargins(str string, inline bool) string { + var ( + topMargin = s.getAsInt(marginTopKey) + rightMargin = s.getAsInt(marginRightKey) + bottomMargin = s.getAsInt(marginBottomKey) + leftMargin = s.getAsInt(marginLeftKey) + + styler termenv.Style + ) + + bgc := s.getAsColor(marginBackgroundKey) + if bgc != noColor { + styler = styler.Background(bgc.color(s.r)) + } + + // Add left and right margin + str = padLeft(str, leftMargin, &styler) + str = padRight(str, rightMargin, &styler) + + // Top/bottom margin + if !inline { + _, width := getLines(str) + spaces := strings.Repeat(" ", width) + + if topMargin > 0 { + str = styler.Styled(strings.Repeat(spaces+"\n", topMargin)) + str + } + if bottomMargin > 0 { + str += styler.Styled(strings.Repeat("\n"+spaces, bottomMargin)) + } + } + + return str +} + +// Apply left padding. +func padLeft(str string, n int, style *termenv.Style) string { + return pad(str, -n, style) +} + +// Apply right padding. +func padRight(str string, n int, style *termenv.Style) string { + return pad(str, n, style) +} + +// pad adds padding to either the left or right side of a string. +// Positive values add to the right side while negative values +// add to the left side. +func pad(str string, n int, style *termenv.Style) string { + if n == 0 { + return str + } + + sp := strings.Repeat(" ", abs(n)) + if style != nil { + sp = style.Styled(sp) + } + + b := strings.Builder{} + l := strings.Split(str, "\n") + + for i := range l { + switch { + // pad right + case n > 0: + b.WriteString(l[i]) + b.WriteString(sp) + // pad left + default: + b.WriteString(sp) + b.WriteString(l[i]) + } + + if i != len(l)-1 { + b.WriteRune('\n') + } + } + + return b.String() +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func abs(a int) int { + if a < 0 { + return -a + } + + return a +} diff --git a/vendor/github.com/charmbracelet/lipgloss/unset.go b/vendor/github.com/charmbracelet/lipgloss/unset.go new file mode 100644 index 000000000..5387bcf73 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/unset.go @@ -0,0 +1,318 @@ +package lipgloss + +// UnsetBold removes the bold style rule, if set. +func (s Style) UnsetBold() Style { + delete(s.rules, boldKey) + return s +} + +// UnsetItalic removes the italic style rule, if set. +func (s Style) UnsetItalic() Style { + delete(s.rules, italicKey) + return s +} + +// UnsetUnderline removes the underline style rule, if set. +func (s Style) UnsetUnderline() Style { + delete(s.rules, underlineKey) + return s +} + +// UnsetStrikethrough removes the strikethrough style rule, if set. +func (s Style) UnsetStrikethrough() Style { + delete(s.rules, strikethroughKey) + return s +} + +// UnsetReverse removes the reverse style rule, if set. +func (s Style) UnsetReverse() Style { + delete(s.rules, reverseKey) + return s +} + +// UnsetBlink removes the blink style rule, if set. +func (s Style) UnsetBlink() Style { + delete(s.rules, blinkKey) + return s +} + +// UnsetFaint removes the faint style rule, if set. +func (s Style) UnsetFaint() Style { + delete(s.rules, faintKey) + return s +} + +// UnsetForeground removes the foreground style rule, if set. +func (s Style) UnsetForeground() Style { + delete(s.rules, foregroundKey) + return s +} + +// UnsetBackground removes the background style rule, if set. +func (s Style) UnsetBackground() Style { + delete(s.rules, backgroundKey) + return s +} + +// UnsetWidth removes the width style rule, if set. +func (s Style) UnsetWidth() Style { + delete(s.rules, widthKey) + return s +} + +// UnsetHeight removes the height style rule, if set. +func (s Style) UnsetHeight() Style { + delete(s.rules, heightKey) + return s +} + +// UnsetAlign removes the horizontal and vertical text alignment style rule, if set. +func (s Style) UnsetAlign() Style { + delete(s.rules, alignHorizontalKey) + delete(s.rules, alignVerticalKey) + return s +} + +// UnsetAlignHorizontal removes the horizontal text alignment style rule, if set. +func (s Style) UnsetAlignHorizontal() Style { + delete(s.rules, alignHorizontalKey) + return s +} + +// UnsetAlignVertical removes the vertical text alignment style rule, if set. +func (s Style) UnsetAlignVertical() Style { + delete(s.rules, alignVerticalKey) + return s +} + +// UnsetPadding removes all padding style rules. +func (s Style) UnsetPadding() Style { + delete(s.rules, paddingLeftKey) + delete(s.rules, paddingRightKey) + delete(s.rules, paddingTopKey) + delete(s.rules, paddingBottomKey) + return s +} + +// UnsetPaddingLeft removes the left padding style rule, if set. +func (s Style) UnsetPaddingLeft() Style { + delete(s.rules, paddingLeftKey) + return s +} + +// UnsetPaddingRight removes the right padding style rule, if set. +func (s Style) UnsetPaddingRight() Style { + delete(s.rules, paddingRightKey) + return s +} + +// UnsetPaddingTop removes the top padding style rule, if set. +func (s Style) UnsetPaddingTop() Style { + delete(s.rules, paddingTopKey) + return s +} + +// UnsetPaddingBottom removes the bottom padding style rule, if set. +func (s Style) UnsetPaddingBottom() Style { + delete(s.rules, paddingBottomKey) + return s +} + +// UnsetColorWhitespace removes the rule for coloring padding, if set. +func (s Style) UnsetColorWhitespace() Style { + delete(s.rules, colorWhitespaceKey) + return s +} + +// UnsetMargins removes all margin style rules. +func (s Style) UnsetMargins() Style { + delete(s.rules, marginLeftKey) + delete(s.rules, marginRightKey) + delete(s.rules, marginTopKey) + delete(s.rules, marginBottomKey) + return s +} + +// UnsetMarginLeft removes the left margin style rule, if set. +func (s Style) UnsetMarginLeft() Style { + delete(s.rules, marginLeftKey) + return s +} + +// UnsetMarginRight removes the right margin style rule, if set. +func (s Style) UnsetMarginRight() Style { + delete(s.rules, marginRightKey) + return s +} + +// UnsetMarginTop removes the top margin style rule, if set. +func (s Style) UnsetMarginTop() Style { + delete(s.rules, marginTopKey) + return s +} + +// UnsetMarginBottom removes the bottom margin style rule, if set. +func (s Style) UnsetMarginBottom() Style { + delete(s.rules, marginBottomKey) + return s +} + +// UnsetMarginBackground removes the margin's background color. Note that the +// margin's background color can be set from the background color of another +// style during inheritance. +func (s Style) UnsetMarginBackground() Style { + delete(s.rules, marginBackgroundKey) + return s +} + +// UnsetBorderStyle removes the border style rule, if set. +func (s Style) UnsetBorderStyle() Style { + delete(s.rules, borderStyleKey) + return s +} + +// UnsetBorderTop removes the border top style rule, if set. +func (s Style) UnsetBorderTop() Style { + delete(s.rules, borderTopKey) + return s +} + +// UnsetBorderRight removes the border right style rule, if set. +func (s Style) UnsetBorderRight() Style { + delete(s.rules, borderRightKey) + return s +} + +// UnsetBorderBottom removes the border bottom style rule, if set. +func (s Style) UnsetBorderBottom() Style { + delete(s.rules, borderBottomKey) + return s +} + +// UnsetBorderLeft removes the border left style rule, if set. +func (s Style) UnsetBorderLeft() Style { + delete(s.rules, borderLeftKey) + return s +} + +// UnsetBorderForeground removes all border foreground color styles, if set. +func (s Style) UnsetBorderForeground() Style { + delete(s.rules, borderTopForegroundKey) + delete(s.rules, borderRightForegroundKey) + delete(s.rules, borderBottomForegroundKey) + delete(s.rules, borderLeftForegroundKey) + return s +} + +// UnsetBorderTopForeground removes the top border foreground color rule, +// if set. +func (s Style) UnsetBorderTopForeground() Style { + delete(s.rules, borderTopForegroundKey) + return s +} + +// UnsetBorderRightForeground removes the right border foreground color rule, +// if set. +func (s Style) UnsetBorderRightForeground() Style { + delete(s.rules, borderRightForegroundKey) + return s +} + +// UnsetBorderBottomForeground removes the bottom border foreground color +// rule, if set. +func (s Style) UnsetBorderBottomForeground() Style { + delete(s.rules, borderBottomForegroundKey) + return s +} + +// UnsetBorderLeftForeground removes the left border foreground color rule, +// if set. +func (s Style) UnsetBorderLeftForeground() Style { + delete(s.rules, borderLeftForegroundKey) + return s +} + +// UnsetBorderBackground removes all border background color styles, if +// set. +func (s Style) UnsetBorderBackground() Style { + delete(s.rules, borderTopBackgroundKey) + delete(s.rules, borderRightBackgroundKey) + delete(s.rules, borderBottomBackgroundKey) + delete(s.rules, borderLeftBackgroundKey) + return s +} + +// UnsetBorderTopBackgroundColor removes the top border background color rule, +// if set. +func (s Style) UnsetBorderTopBackgroundColor() Style { + delete(s.rules, borderTopBackgroundKey) + return s +} + +// UnsetBorderRightBackground removes the right border background color +// rule, if set. +func (s Style) UnsetBorderRightBackground() Style { + delete(s.rules, borderRightBackgroundKey) + return s +} + +// UnsetBorderBottomBackground removes the bottom border background color +// rule, if set. +func (s Style) UnsetBorderBottomBackground() Style { + delete(s.rules, borderBottomBackgroundKey) + return s +} + +// UnsetBorderLeftBackground removes the left border color rule, if set. +func (s Style) UnsetBorderLeftBackground() Style { + delete(s.rules, borderLeftBackgroundKey) + return s +} + +// UnsetInline removes the inline style rule, if set. +func (s Style) UnsetInline() Style { + delete(s.rules, inlineKey) + return s +} + +// UnsetMaxWidth removes the max width style rule, if set. +func (s Style) UnsetMaxWidth() Style { + delete(s.rules, maxWidthKey) + return s +} + +// UnsetMaxHeight removes the max height style rule, if set. +func (s Style) UnsetMaxHeight() Style { + delete(s.rules, maxHeightKey) + return s +} + +// UnsetTabWidth removes the tab width style rule, if set. +func (s Style) UnsetTabWidth() Style { + delete(s.rules, tabWidthKey) + return s +} + +// UnsetUnderlineSpaces removes the value set by UnderlineSpaces. +func (s Style) UnsetUnderlineSpaces() Style { + delete(s.rules, underlineSpacesKey) + return s +} + +// UnsetStrikethroughSpaces removes the value set by StrikethroughSpaces. +func (s Style) UnsetStrikethroughSpaces() Style { + delete(s.rules, strikethroughSpacesKey) + return s +} + +// UnsetTransform removes the value set by Transform. +func (s Style) UnsetTransform() Style { + delete(s.rules, transformKey) + return s +} + +// UnsetString sets the underlying string value to the empty string. +func (s Style) UnsetString() Style { + s.value = "" + return s +} diff --git a/vendor/github.com/charmbracelet/lipgloss/whitespace.go b/vendor/github.com/charmbracelet/lipgloss/whitespace.go new file mode 100644 index 000000000..19656871d --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/whitespace.go @@ -0,0 +1,83 @@ +package lipgloss + +import ( + "strings" + + "github.com/charmbracelet/x/exp/term/ansi" + "github.com/muesli/termenv" +) + +// whitespace is a whitespace renderer. +type whitespace struct { + re *Renderer + style termenv.Style + chars string +} + +// newWhitespace creates a new whitespace renderer. The order of the options +// matters, if you're using WithWhitespaceRenderer, make sure it comes first as +// other options might depend on it. +func newWhitespace(r *Renderer, opts ...WhitespaceOption) *whitespace { + w := &whitespace{ + re: r, + style: r.ColorProfile().String(), + } + for _, opt := range opts { + opt(w) + } + return w +} + +// Render whitespaces. +func (w whitespace) render(width int) string { + if w.chars == "" { + w.chars = " " + } + + r := []rune(w.chars) + j := 0 + b := strings.Builder{} + + // Cycle through runes and print them into the whitespace. + for i := 0; i < width; { + b.WriteRune(r[j]) + j++ + if j >= len(r) { + j = 0 + } + i += ansi.StringWidth(string(r[j])) + } + + // Fill any extra gaps white spaces. This might be necessary if any runes + // are more than one cell wide, which could leave a one-rune gap. + short := width - ansi.StringWidth(b.String()) + if short > 0 { + b.WriteString(strings.Repeat(" ", short)) + } + + return w.style.Styled(b.String()) +} + +// WhitespaceOption sets a styling rule for rendering whitespace. +type WhitespaceOption func(*whitespace) + +// WithWhitespaceForeground sets the color of the characters in the whitespace. +func WithWhitespaceForeground(c TerminalColor) WhitespaceOption { + return func(w *whitespace) { + w.style = w.style.Foreground(c.color(w.re)) + } +} + +// WithWhitespaceBackground sets the background color of the whitespace. +func WithWhitespaceBackground(c TerminalColor) WhitespaceOption { + return func(w *whitespace) { + w.style = w.style.Background(c.color(w.re)) + } +} + +// WithWhitespaceChars sets the characters to be rendered in the whitespace. +func WithWhitespaceChars(s string) WhitespaceOption { + return func(w *whitespace) { + w.chars = s + } +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/LICENSE b/vendor/github.com/charmbracelet/x/exp/term/LICENSE new file mode 100644 index 000000000..65a5654e2 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Charmbracelet, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/ansi.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/ansi.go new file mode 100644 index 000000000..48d873c3f --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/ansi.go @@ -0,0 +1,11 @@ +package ansi + +import "io" + +// Execute is a function that "execute" the given escape sequence by writing it +// to the provided output writter. +// +// This is a syntactic sugar over [io.WriteString]. +func Execute(w io.Writer, s string) (int, error) { + return io.WriteString(w, s) +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/ascii.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/ascii.go new file mode 100644 index 000000000..188582f7e --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/ascii.go @@ -0,0 +1,8 @@ +package ansi + +const ( + // SP is the space character (Char: \x20). + SP = 0x20 + // DEL is the delete character (Caret: ^?, Char: \x7f). + DEL = 0x7F +) diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/background.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/background.go new file mode 100644 index 000000000..f519af08f --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/background.go @@ -0,0 +1,61 @@ +package ansi + +import ( + "image/color" +) + +// SetForegroundColor returns a sequence that sets the default terminal +// foreground color. +// +// OSC 10 ; color ST +// OSC 10 ; color BEL +// +// Where color is the encoded color number. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +func SetForegroundColor(c color.Color) string { + return "\x1b]10;" + colorToHexString(c) + "\x07" +} + +// RequestForegroundColor is a sequence that requests the current default +// terminal foreground color. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +const RequestForegroundColor = "\x1b]10;?\x07" + +// SetBackgroundColor returns a sequence that sets the default terminal +// background color. +// +// OSC 11 ; color ST +// OSC 11 ; color BEL +// +// Where color is the encoded color number. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +func SetBackgroundColor(c color.Color) string { + return "\x1b]11;" + colorToHexString(c) + "\x07" +} + +// RequestBackgroundColor is a sequence that requests the current default +// terminal background color. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +const RequestBackgroundColor = "\x1b]11;?\x07" + +// SetCursorColor returns a sequence that sets the terminal cursor color. +// +// OSC 12 ; color ST +// OSC 12 ; color BEL +// +// Where color is the encoded color number. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +func SetCursorColor(c color.Color) string { + return "\x1b]12;" + colorToHexString(c) + "\x07" +} + +// RequestCursorColor is a sequence that requests the current terminal cursor +// color. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +const RequestCursorColor = "\x1b]12;?\x07" diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/c0.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/c0.go new file mode 100644 index 000000000..13e3c6c31 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/c0.go @@ -0,0 +1,72 @@ +package ansi + +// C0 control characters. +// +// These range from (0x00-0x1F) as defined in ISO 646 (ASCII). +// See: https://en.wikipedia.org/wiki/C0_and_C1_control_codes +const ( + // NUL is the null character (Caret: ^@, Char: \0). + NUL = 0x00 + // SOH is the start of heading character (Caret: ^A). + SOH = 0x01 + // STX is the start of text character (Caret: ^B). + STX = 0x02 + // ETX is the end of text character (Caret: ^C). + ETX = 0x03 + // EOT is the end of transmission character (Caret: ^D). + EOT = 0x04 + // ENQ is the enquiry character (Caret: ^E). + ENQ = 0x05 + // ACK is the acknowledge character (Caret: ^F). + ACK = 0x06 + // BEL is the bell character (Caret: ^G, Char: \a). + BEL = 0x07 + // BS is the backspace character (Caret: ^H, Char: \b). + BS = 0x08 + // HT is the horizontal tab character (Caret: ^I, Char: \t). + HT = 0x09 + // LF is the line feed character (Caret: ^J, Char: \n). + LF = 0x0A + // VT is the vertical tab character (Caret: ^K, Char: \v). + VT = 0x0B + // FF is the form feed character (Caret: ^L, Char: \f). + FF = 0x0C + // CR is the carriage return character (Caret: ^M, Char: \r). + CR = 0x0D + // SO is the shift out character (Caret: ^N). + SO = 0x0E + // SI is the shift in character (Caret: ^O). + SI = 0x0F + // DLE is the data link escape character (Caret: ^P). + DLE = 0x10 + // DC1 is the device control 1 character (Caret: ^Q). + DC1 = 0x11 + // DC2 is the device control 2 character (Caret: ^R). + DC2 = 0x12 + // DC3 is the device control 3 character (Caret: ^S). + DC3 = 0x13 + // DC4 is the device control 4 character (Caret: ^T). + DC4 = 0x14 + // NAK is the negative acknowledge character (Caret: ^U). + NAK = 0x15 + // SYN is the synchronous idle character (Caret: ^V). + SYN = 0x16 + // ETB is the end of transmission block character (Caret: ^W). + ETB = 0x17 + // CAN is the cancel character (Caret: ^X). + CAN = 0x18 + // EM is the end of medium character (Caret: ^Y). + EM = 0x19 + // SUB is the substitute character (Caret: ^Z). + SUB = 0x1A + // ESC is the escape character (Caret: ^[, Char: \e). + ESC = 0x1B + // FS is the file separator character (Caret: ^\). + FS = 0x1C + // GS is the group separator character (Caret: ^]). + GS = 0x1D + // RS is the record separator character (Caret: ^^). + RS = 0x1E + // US is the unit separator character (Caret: ^_). + US = 0x1F +) diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/c1.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/c1.go new file mode 100644 index 000000000..71058f539 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/c1.go @@ -0,0 +1,72 @@ +package ansi + +// C1 control characters. +// +// These range from (0x80-0x9F) as defined in ISO 6429 (ECMA-48). +// See: https://en.wikipedia.org/wiki/C0_and_C1_control_codes +const ( + // PAD is the padding character. + PAD = 0x80 + // HOP is the high octet preset character. + HOP = 0x81 + // BPH is the break permitted here character. + BPH = 0x82 + // NBH is the no break here character. + NBH = 0x83 + // IND is the index character. + IND = 0x84 + // NEL is the next line character. + NEL = 0x85 + // SSA is the start of selected area character. + SSA = 0x86 + // ESA is the end of selected area character. + ESA = 0x87 + // HTS is the horizontal tab set character. + HTS = 0x88 + // HTJ is the horizontal tab with justification character. + HTJ = 0x89 + // VTS is the vertical tab set character. + VTS = 0x8A + // PLD is the partial line forward character. + PLD = 0x8B + // PLU is the partial line backward character. + PLU = 0x8C + // RI is the reverse index character. + RI = 0x8D + // SS2 is the single shift 2 character. + SS2 = 0x8E + // SS3 is the single shift 3 character. + SS3 = 0x8F + // DCS is the device control string character. + DCS = 0x90 + // PU1 is the private use 1 character. + PU1 = 0x91 + // PU2 is the private use 2 character. + PU2 = 0x92 + // STS is the set transmit state character. + STS = 0x93 + // CCH is the cancel character. + CCH = 0x94 + // MW is the message waiting character. + MW = 0x95 + // SPA is the start of guarded area character. + SPA = 0x96 + // EPA is the end of guarded area character. + EPA = 0x97 + // SOS is the start of string character. + SOS = 0x98 + // SGCI is the single graphic character introducer character. + SGCI = 0x99 + // SCI is the single character introducer character. + SCI = 0x9A + // CSI is the control sequence introducer character. + CSI = 0x9B + // ST is the string terminator character. + ST = 0x9C + // OSC is the operating system command character. + OSC = 0x9D + // PM is the privacy message character. + PM = 0x9E + // APC is the application program command character. + APC = 0x9F +) diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/clipboard.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/clipboard.go new file mode 100644 index 000000000..94d26c366 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/clipboard.go @@ -0,0 +1,75 @@ +package ansi + +import "encoding/base64" + +// Clipboard names. +const ( + SystemClipboard = 'c' + PrimaryClipboard = 'p' +) + +// SetClipboard returns a sequence for manipulating the clipboard. +// +// OSC 52 ; Pc ; Pd ST +// OSC 52 ; Pc ; Pd BEL +// +// Where Pc is the clipboard name and Pd is the base64 encoded data. +// Empty data or invalid base64 data will reset the clipboard. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +func SetClipboard(c byte, d string) string { + if d != "" { + d = base64.StdEncoding.EncodeToString([]byte(d)) + } + return "\x1b]52;" + string(c) + ";" + d + "\x07" +} + +// SetSystemClipboard returns a sequence for setting the system clipboard. +// +// This is equivalent to SetClipboard(SystemClipboard, d). +func SetSystemClipboard(d string) string { + return SetClipboard(SystemClipboard, d) +} + +// SetPrimaryClipboard returns a sequence for setting the primary clipboard. +// +// This is equivalent to SetClipboard(PrimaryClipboard, d). +func SetPrimaryClipboard(d string) string { + return SetClipboard(PrimaryClipboard, d) +} + +// ResetClipboard returns a sequence for resetting the clipboard. +// +// This is equivalent to SetClipboard(c, ""). +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +func ResetClipboard(c byte) string { + return SetClipboard(c, "") +} + +// ResetSystemClipboard is a sequence for resetting the system clipboard. +// +// This is equivalent to ResetClipboard(SystemClipboard). +const ResetSystemClipboard = "\x1b]52;c;\x07" + +// ResetPrimaryClipboard is a sequence for resetting the primary clipboard. +// +// This is equivalent to ResetClipboard(PrimaryClipboard). +const ResetPrimaryClipboard = "\x1b]52;p;\x07" + +// RequestClipboard returns a sequence for requesting the clipboard. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +func RequestClipboard(c byte) string { + return "\x1b]52;" + string(c) + ";?\x07" +} + +// RequestSystemClipboard is a sequence for requesting the system clipboard. +// +// This is equivalent to RequestClipboard(SystemClipboard). +const RequestSystemClipboard = "\x1b]52;c;?\x07" + +// RequestPrimaryClipboard is a sequence for requesting the primary clipboard. +// +// This is equivalent to RequestClipboard(PrimaryClipboard). +const RequestPrimaryClipboard = "\x1b]52;p;?\x07" diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/color.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/color.go new file mode 100644 index 000000000..11419768a --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/color.go @@ -0,0 +1,191 @@ +package ansi + +import ( + "image/color" +) + +// Technically speaking, the 16 basic ANSI colors are arbitrary and can be +// customized at the terminal level. Given that, we're returning what we feel +// are good defaults. +// +// This could also be a slice, but we use a map to make the mappings very +// explicit. +// +// See: https://www.ditig.com/publications/256-colors-cheat-sheet +var lowANSI = map[uint32]uint32{ + 0: 0x000000, // black + 1: 0x800000, // red + 2: 0x008000, // green + 3: 0x808000, // yellow + 4: 0x000080, // blue + 5: 0x800080, // magenta + 6: 0x008080, // cyan + 7: 0xc0c0c0, // white + 8: 0x808080, // bright black + 9: 0xff0000, // bright red + 10: 0x00ff00, // bright green + 11: 0xffff00, // bright yellow + 12: 0x0000ff, // bright blue + 13: 0xff00ff, // bright magenta + 14: 0x00ffff, // bright cyan + 15: 0xffffff, // bright white +} + +// Color is a color that can be used in a terminal. ANSI (including +// ANSI256) and 24-bit "true colors" fall under this category. +type Color interface { + color.Color +} + +// BasicColor is an ANSI 3-bit or 4-bit color with a value from 0 to 15. +type BasicColor uint8 + +var _ Color = BasicColor(0) + +const ( + // Black is the ANSI black color. + Black BasicColor = iota + + // Red is the ANSI red color. + Red + + // Green is the ANSI green color. + Green + + // Yellow is the ANSI yellow color. + Yellow + + // Blue is the ANSI blue color. + Blue + + // Magenta is the ANSI magenta color. + Magenta + + // Cyan is the ANSI cyan color. + Cyan + + // White is the ANSI white color. + White + + // BrightBlack is the ANSI bright black color. + BrightBlack + + // BrightRed is the ANSI bright red color. + BrightRed + + // BrightGreen is the ANSI bright green color. + BrightGreen + + // BrightYellow is the ANSI bright yellow color. + BrightYellow + + // BrightBlue is the ANSI bright blue color. + BrightBlue + + // BrightMagenta is the ANSI bright magenta color. + BrightMagenta + + // BrightCyan is the ANSI bright cyan color. + BrightCyan + + // BrightWhite is the ANSI bright white color. + BrightWhite +) + +// RGBA returns the red, green, blue and alpha components of the color. It +// satisfies the color.Color interface. +func (c BasicColor) RGBA() (uint32, uint32, uint32, uint32) { + ansi := uint32(c) + if ansi > 15 { + return 0, 0, 0, 0xff + } + + r, g, b := ansiToRGB(ansi) + r |= r << 8 + g |= g << 8 + b |= b << 8 + return r, g, b, 0xff00 +} + +// ExtendedColor is an ANSI 256 (8-bit) color with a value from 0 to 255. +type ExtendedColor uint8 + +var _ Color = ExtendedColor(0) + +// RGBA returns the red, green, blue and alpha components of the color. It +// satisfies the color.Color interface. +func (c ExtendedColor) RGBA() (uint32, uint32, uint32, uint32) { + r, g, b := ansiToRGB(uint32(c)) + r |= r << 8 + g |= g << 8 + b |= b << 8 + return r, g, b, 0xff00 +} + +// TrueColor is a 24-bit color that can be used in the terminal. +// This can be used to represent RGB colors. +// +// For example, the color red can be represented as: +// +// TrueColor(0xff0000) +type TrueColor uint32 + +var _ Color = TrueColor(0) + +// RGBA returns the red, green, blue and alpha components of the color. It +// satisfies the color.Color interface. +func (c TrueColor) RGBA() (uint32, uint32, uint32, uint32) { + r, g, b := hexToRGB(uint32(c)) + r |= r << 8 + g |= g << 8 + b |= b << 8 + return r, g, b, 0xff00 +} + +// ansiToRGB converts an ANSI color to a 24-bit RGB color. +// +// r, g, b := ansiToRGB(57) +func ansiToRGB(ansi uint32) (uint32, uint32, uint32) { + // For out-of-range values return black. + if ansi > 255 { + return 0, 0, 0 + } + + // Low ANSI. + if ansi < 16 { + h, ok := lowANSI[ansi] + if !ok { + return 0, 0, 0 + } + r, g, b := hexToRGB(h) + return r, g, b + } + + // Grays. + if ansi > 231 { + s := (ansi-232)*10 + 8 + return s, s, s + } + + // ANSI256. + n := ansi - 16 + b := n % 6 + g := (n - b) / 6 % 6 + r := (n - b - g*6) / 36 % 6 + for _, v := range []*uint32{&r, &g, &b} { + if *v > 0 { + c := *v*40 + 55 + *v = c + } + } + + return r, g, b +} + +// hexToRGB converts a number in hexadecimal format to red, green, and blue +// values. +// +// r, g, b := hexToRGB(0x0000FF) +func hexToRGB(hex uint32) (uint32, uint32, uint32) { + return hex >> 16, hex >> 8 & 0xff, hex & 0xff +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/csi.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/csi.go new file mode 100644 index 000000000..9879f0c27 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/csi.go @@ -0,0 +1,141 @@ +package ansi + +import ( + "bytes" + "strconv" + + "github.com/charmbracelet/x/exp/term/ansi/parser" +) + +// CsiSequence represents a control sequence introducer (CSI) sequence. +// +// The sequence starts with a CSI sequence, CSI (0x9B) in a 8-bit environment +// or ESC [ (0x1B 0x5B) in a 7-bit environment, followed by any number of +// parameters in the range of 0x30-0x3F, then by any number of intermediate +// byte in the range of 0x20-0x2F, then finally with a single final byte in the +// range of 0x20-0x7E. +// +// CSI P..P I..I F +// +// See ECMA-48 § 5.4. +type CsiSequence struct { + // Params contains the raw parameters of the sequence. + // This is a slice of integers, where each integer is a 32-bit integer + // containing the parameter value in the lower 31 bits and a flag in the + // most significant bit indicating whether there are more sub-parameters. + Params []int + + // Cmd contains the raw command of the sequence. + // The command is a 32-bit integer containing the CSI command byte in the + // lower 8 bits, the private marker in the next 8 bits, and the intermediate + // byte in the next 8 bits. + // + // CSI ? u + // + // Is represented as: + // + // 'u' | '?' << 8 + Cmd int +} + +var _ Sequence = CsiSequence{} + +// Marker returns the marker byte of the CSI sequence. +// This is always gonna be one of the following '<' '=' '>' '?' and in the +// range of 0x3C-0x3F. +// Zero is returned if the sequence does not have a marker. +func (s CsiSequence) Marker() int { + return parser.Marker(s.Cmd) +} + +// Intermediate returns the intermediate byte of the CSI sequence. +// An intermediate byte is in the range of 0x20-0x2F. This includes these +// characters from ' ', '!', '"', '#', '$', '%', '&', ”', '(', ')', '*', '+', +// ',', '-', '.', '/'. +// Zero is returned if the sequence does not have an intermediate byte. +func (s CsiSequence) Intermediate() int { + return parser.Intermediate(s.Cmd) +} + +// Command returns the command byte of the CSI sequence. +func (s CsiSequence) Command() int { + return parser.Command(s.Cmd) +} + +// Param returns the parameter at the given index. +// It returns -1 if the parameter does not exist. +func (s CsiSequence) Param(i int) int { + return parser.Param(s.Params, i) +} + +// HasMore returns true if the parameter has more sub-parameters. +func (s CsiSequence) HasMore(i int) bool { + return parser.HasMore(s.Params, i) +} + +// Subparams returns the sub-parameters of the given parameter. +// It returns nil if the parameter does not exist. +func (s CsiSequence) Subparams(i int) []int { + return parser.Subparams(s.Params, i) +} + +// Len returns the number of parameters in the sequence. +// This will return the number of parameters in the sequence, excluding any +// sub-parameters. +func (s CsiSequence) Len() int { + return parser.Len(s.Params) +} + +// Range iterates over the parameters of the sequence and calls the given +// function for each parameter. +// The function should return false to stop the iteration. +func (s CsiSequence) Range(fn func(i int, param int, hasMore bool) bool) { + parser.Range(s.Params, fn) +} + +// Clone returns a copy of the CSI sequence. +func (s CsiSequence) Clone() Sequence { + return CsiSequence{ + Params: append([]int(nil), s.Params...), + Cmd: s.Cmd, + } +} + +// String returns a string representation of the sequence. +// The string will always be in the 7-bit format i.e (ESC [ P..P I..I F). +func (s CsiSequence) String() string { + return s.buffer().String() +} + +// buffer returns a buffer containing the sequence. +func (s CsiSequence) buffer() *bytes.Buffer { + var b bytes.Buffer + b.WriteString("\x1b[") + if m := s.Marker(); m != 0 { + b.WriteByte(byte(m)) + } + s.Range(func(i, param int, hasMore bool) bool { + if param >= 0 { + b.WriteString(strconv.Itoa(param)) + } + if i < len(s.Params)-1 { + if hasMore { + b.WriteByte(':') + } else { + b.WriteByte(';') + } + } + return true + }) + if i := s.Intermediate(); i != 0 { + b.WriteByte(byte(i)) + } + b.WriteByte(byte(s.Command())) + return &b +} + +// Bytes returns the byte representation of the sequence. +// The bytes will always be in the 7-bit format i.e (ESC [ P..P I..I F). +func (s CsiSequence) Bytes() []byte { + return s.buffer().Bytes() +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/ctrl.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/ctrl.go new file mode 100644 index 000000000..21beb9cd1 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/ctrl.go @@ -0,0 +1,17 @@ +package ansi + +// RequestXTVersion is a control sequence that requests the terminal's XTVERSION. It responds with a DSR sequence identifying the version. +// +// CSI > Ps q +// DCS > | text ST +// +// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-PC-Style-Function-Keys +const RequestXTVersion = "\x1b[>0q" + +// RequestPrimaryDeviceAttributes is a control sequence that requests the +// terminal's primary device attributes (DA1). +// +// CSI c +// +// See https://vt100.net/docs/vt510-rm/DA1.html +const RequestPrimaryDeviceAttributes = "\x1b[c" diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/cursor.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/cursor.go new file mode 100644 index 000000000..5f010fd4c --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/cursor.go @@ -0,0 +1,190 @@ +package ansi + +import "strconv" + +// SaveCursor (DECSC) is an escape sequence that saves the current cursor +// position. +// +// ESC 7 +// +// See: https://vt100.net/docs/vt510-rm/DECSC.html +const SaveCursor = "\x1b7" + +// RestoreCursor (DECRC) is an escape sequence that restores the cursor +// position. +// +// ESC 8 +// +// See: https://vt100.net/docs/vt510-rm/DECRC.html +const RestoreCursor = "\x1b8" + +// RequestCursorPosition (CPR) is an escape sequence that requests the current +// cursor position. +// +// CSI 6 n +// +// The terminal will report the cursor position as a CSI sequence in the +// following format: +// +// CSI Pl ; Pc R +// +// Where Pl is the line number and Pc is the column number. +// See: https://vt100.net/docs/vt510-rm/CPR.html +const RequestCursorPosition = "\x1b[6n" + +// RequestExtendedCursorPosition (DECXCPR) is a sequence for requesting the +// cursor position report including the current page number. +// +// CSI ? 6 n +// +// The terminal will report the cursor position as a CSI sequence in the +// following format: +// +// CSI ? Pl ; Pc ; Pp R +// +// Where Pl is the line number, Pc is the column number, and Pp is the page +// number. +// See: https://vt100.net/docs/vt510-rm/DECXCPR.html +const RequestExtendedCursorPosition = "\x1b[?6n" + +// CursorUp (CUU) returns a sequence for moving the cursor up n cells. +// +// CSI n A +// +// See: https://vt100.net/docs/vt510-rm/CUU.html +func CursorUp(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "A" +} + +// CursorUp1 is a sequence for moving the cursor up one cell. +// +// This is equivalent to CursorUp(1). +const CursorUp1 = "\x1b[A" + +// CursorDown (CUD) returns a sequence for moving the cursor down n cells. +// +// CSI n B +// +// See: https://vt100.net/docs/vt510-rm/CUD.html +func CursorDown(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "B" +} + +// CursorDown1 is a sequence for moving the cursor down one cell. +// +// This is equivalent to CursorDown(1). +const CursorDown1 = "\x1b[B" + +// CursorRight (CUF) returns a sequence for moving the cursor right n cells. +// +// CSI n C +// +// See: https://vt100.net/docs/vt510-rm/CUF.html +func CursorRight(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "C" +} + +// CursorRight1 is a sequence for moving the cursor right one cell. +// +// This is equivalent to CursorRight(1). +const CursorRight1 = "\x1b[C" + +// CursorLeft (CUB) returns a sequence for moving the cursor left n cells. +// +// CSI n D +// +// See: https://vt100.net/docs/vt510-rm/CUB.html +func CursorLeft(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "D" +} + +// CursorLeft1 is a sequence for moving the cursor left one cell. +// +// This is equivalent to CursorLeft(1). +const CursorLeft1 = "\x1b[D" + +// CursorNextLine (CNL) returns a sequence for moving the cursor to the +// beginning of the next line n times. +// +// CSI n E +// +// See: https://vt100.net/docs/vt510-rm/CNL.html +func CursorNextLine(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "E" +} + +// CursorPreviousLine (CPL) returns a sequence for moving the cursor to the +// beginning of the previous line n times. +// +// CSI n F +// +// See: https://vt100.net/docs/vt510-rm/CPL.html +func CursorPreviousLine(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "F" +} + +// MoveCursor (CUP) returns a sequence for moving the cursor to the given row +// and column. +// +// CSI n ; m H +// +// See: https://vt100.net/docs/vt510-rm/CUP.html +func MoveCursor(row, col int) string { + if row < 0 { + row = 0 + } + if col < 0 { + col = 0 + } + return "\x1b[" + strconv.Itoa(row) + ";" + strconv.Itoa(col) + "H" +} + +// MoveCursorOrigin is a sequence for moving the cursor to the upper left +// corner of the screen. This is equivalent to MoveCursor(1, 1). +const MoveCursorOrigin = "\x1b[1;1H" + +// SaveCursorPosition (SCP or SCOSC) is a sequence for saving the cursor +// position. +// +// CSI s +// +// This acts like Save, except the page number where the cursor is located is +// not saved. +// +// See: https://vt100.net/docs/vt510-rm/SCOSC.html +const SaveCursorPosition = "\x1b[s" + +// RestoreCursorPosition (RCP or SCORC) is a sequence for restoring the cursor +// position. +// +// CSI u +// +// This acts like Restore, except the cursor stays on the same page where the +// cursor was saved. +// +// See: https://vt100.net/docs/vt510-rm/SCORC.html +const RestoreCursorPosition = "\x1b[u" diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/dcs.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/dcs.go new file mode 100644 index 000000000..896dfb11e --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/dcs.go @@ -0,0 +1,148 @@ +package ansi + +import ( + "bytes" + "strconv" + + "github.com/charmbracelet/x/exp/term/ansi/parser" +) + +// DcsSequence represents a Device Control String (DCS) escape sequence. +// +// The DCS sequence is used to send device control strings to the terminal. The +// sequence starts with the C1 control code character DCS (0x9B) or ESC P in +// 7-bit environments, followed by parameter bytes, intermediate bytes, a +// command byte, followed by data bytes, and ends with the C1 control code +// character ST (0x9C) or ESC \ in 7-bit environments. +// +// This follows the parameter string format. +// See ECMA-48 § 5.4.1 +type DcsSequence struct { + // Params contains the raw parameters of the sequence. + // This is a slice of integers, where each integer is a 32-bit integer + // containing the parameter value in the lower 31 bits and a flag in the + // most significant bit indicating whether there are more sub-parameters. + Params []int + + // Data contains the string raw data of the sequence. + // This is the data between the final byte and the escape sequence terminator. + Data []byte + + // Cmd contains the raw command of the sequence. + // The command is a 32-bit integer containing the DCS command byte in the + // lower 8 bits, the private marker in the next 8 bits, and the intermediate + // byte in the next 8 bits. + // + // DCS > 0 ; 1 $ r ST + // + // Is represented as: + // + // 'r' | '>' << 8 | '$' << 16 + Cmd int +} + +var _ Sequence = DcsSequence{} + +// Marker returns the marker byte of the DCS sequence. +// This is always gonna be one of the following '<' '=' '>' '?' and in the +// range of 0x3C-0x3F. +// Zero is returned if the sequence does not have a marker. +func (s DcsSequence) Marker() int { + return parser.Marker(s.Cmd) +} + +// Intermediate returns the intermediate byte of the DCS sequence. +// An intermediate byte is in the range of 0x20-0x2F. This includes these +// characters from ' ', '!', '"', '#', '$', '%', '&', ”', '(', ')', '*', '+', +// ',', '-', '.', '/'. +// Zero is returned if the sequence does not have an intermediate byte. +func (s DcsSequence) Intermediate() int { + return parser.Intermediate(s.Cmd) +} + +// Command returns the command byte of the CSI sequence. +func (s DcsSequence) Command() int { + return parser.Command(s.Cmd) +} + +// Param returns the parameter at the given index. +// It returns -1 if the parameter does not exist. +func (s DcsSequence) Param(i int) int { + return parser.Param(s.Params, i) +} + +// HasMore returns true if the parameter has more sub-parameters. +func (s DcsSequence) HasMore(i int) bool { + return parser.HasMore(s.Params, i) +} + +// Subparams returns the sub-parameters of the given parameter. +// It returns nil if the parameter does not exist. +func (s DcsSequence) Subparams(i int) []int { + return parser.Subparams(s.Params, i) +} + +// Len returns the number of parameters in the sequence. +// This will return the number of parameters in the sequence, excluding any +// sub-parameters. +func (s DcsSequence) Len() int { + return parser.Len(s.Params) +} + +// Range iterates over the parameters of the sequence and calls the given +// function for each parameter. +// The function should return false to stop the iteration. +func (s DcsSequence) Range(fn func(i int, param int, hasMore bool) bool) { + parser.Range(s.Params, fn) +} + +// Clone returns a copy of the DCS sequence. +func (s DcsSequence) Clone() Sequence { + return DcsSequence{ + Params: append([]int(nil), s.Params...), + Data: append([]byte(nil), s.Data...), + Cmd: s.Cmd, + } +} + +// String returns a string representation of the sequence. +// The string will always be in the 7-bit format i.e (ESC P p..p i..i f ESC \). +func (s DcsSequence) String() string { + return s.buffer().String() +} + +// buffer returns a buffer containing the sequence. +func (s DcsSequence) buffer() *bytes.Buffer { + var b bytes.Buffer + b.WriteString("\x1bP") + if m := s.Marker(); m != 0 { + b.WriteByte(byte(m)) + } + s.Range(func(i, param int, hasMore bool) bool { + if param >= -1 { + b.WriteString(strconv.Itoa(param)) + } + if i < len(s.Params)-1 { + if hasMore { + b.WriteByte(':') + } else { + b.WriteByte(';') + } + } + return true + }) + if i := s.Intermediate(); i != 0 { + b.WriteByte(byte(i)) + } + b.WriteByte(byte(s.Command())) + b.Write(s.Data) + b.WriteByte(ESC) + b.WriteByte('\\') + return &b +} + +// Bytes returns the byte representation of the sequence. +// The bytes will always be in the 7-bit format i.e (ESC P p..p i..i F ESC \). +func (s DcsSequence) Bytes() []byte { + return s.buffer().Bytes() +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/doc.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/doc.go new file mode 100644 index 000000000..e955e9f1b --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/doc.go @@ -0,0 +1,7 @@ +// Package ansi defines common ANSI escape sequences based on the ECMA-48 +// specs. +// +// All sequences use 7-bit C1 control codes, which are supported by most +// terminal emulators. OSC sequences are terminated by a BEL for wider +// compatibility with terminals. +package ansi diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/hyperlink.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/hyperlink.go new file mode 100644 index 000000000..323bfe932 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/hyperlink.go @@ -0,0 +1,28 @@ +package ansi + +import "strings" + +// SetHyperlink returns a sequence for starting a hyperlink. +// +// OSC 8 ; Params ; Uri ST +// OSC 8 ; Params ; Uri BEL +// +// To reset the hyperlink, omit the URI. +// +// See: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda +func SetHyperlink(uri string, params ...string) string { + var p string + if len(params) > 0 { + p = strings.Join(params, ":") + } + return "\x1b]8;" + p + ";" + uri + "\x07" +} + +// ResetHyperlink returns a sequence for resetting the hyperlink. +// +// This is equivalent to SetHyperlink("", params...). +// +// See: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda +func ResetHyperlink(params ...string) string { + return SetHyperlink("", params...) +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/kitty.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/kitty.go new file mode 100644 index 000000000..5bf89814a --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/kitty.go @@ -0,0 +1,58 @@ +package ansi + +import "strconv" + +// Kitty keyboard protocol progressive enhancement flags. +// See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement +const ( + KittyDisambiguateEscapeCodes = 1 << iota + KittyReportEventTypes + KittyReportAlternateKeys + KittyReportAllKeys + KittyReportAssociatedKeys + + KittyAllFlags = KittyDisambiguateEscapeCodes | KittyReportEventTypes | + KittyReportAlternateKeys | KittyReportAllKeys | KittyReportAssociatedKeys +) + +// RequestKittyKeyboard is a sequence to request the terminal Kitty keyboard +// protocol enabled flags. +// +// See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/ +const RequestKittyKeyboard = "\x1b[?u" + +// PushKittyKeyboard returns a sequence to push the given flags to the terminal +// Kitty Keyboard stack. +// +// CSI > flags u +// +// See https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement +func PushKittyKeyboard(flags int) string { + var f string + if flags > 0 { + f = strconv.Itoa(flags) + } + + return "\x1b[>" + f + "u" +} + +// DisableKittyKeyboard is a sequence to push zero into the terminal Kitty +// Keyboard stack to disable the protocol. +// +// This is equivalent to PushKittyKeyboard(0). +const DisableKittyKeyboard = "\x1b[>0u" + +// PopKittyKeyboard returns a sequence to pop n number of flags from the +// terminal Kitty Keyboard stack. +// +// CSI < flags u +// +// See https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement +func PopKittyKeyboard(n int) string { + var num string + if n > 0 { + num = strconv.Itoa(n) + } + + return "\x1b[<" + num + "u" +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/mode.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/mode.go new file mode 100644 index 000000000..4c39e78d3 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/mode.go @@ -0,0 +1,132 @@ +package ansi + +// This file define uses multiple sequences to set (SM), reset (RM), and request +// (DECRQM) different ANSI and DEC modes. +// +// See: https://vt100.net/docs/vt510-rm/SM.html +// See: https://vt100.net/docs/vt510-rm/RM.html +// See: https://vt100.net/docs/vt510-rm/DECRQM.html +// +// The terminal then responds to the request with a Report Mode function +// (DECRPM) in the format: +// +// ANSI format: +// +// CSI Pa ; Ps ; $ y +// +// DEC format: +// +// CSI ? Pa ; Ps $ y +// +// Where Pa is the mode number, and Ps is the mode value. +// See: https://vt100.net/docs/vt510-rm/DECRPM.html + +// Application Cursor Keys (DECCKM) is a mode that determines whether the +// cursor keys send ANSI cursor sequences or application sequences. +// +// See: https://vt100.net/docs/vt510-rm/DECCKM.html +const ( + EnableCursorKeys = "\x1b[?1h" + DisableCursorKeys = "\x1b[?1l" + RequestCursorKeys = "\x1b[?1$p" +) + +// Text Cursor Enable Mode (DECTCEM) is a mode that shows/hides the cursor. +// +// See: https://vt100.net/docs/vt510-rm/DECTCEM.html +const ( + ShowCursor = "\x1b[?25h" + HideCursor = "\x1b[?25l" + RequestCursorVisibility = "\x1b[?25$p" +) + +// VT Mouse Tracking is a mode that determines whether the mouse reports on +// button press and release. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +const ( + EnableMouse = "\x1b[?1000h" + DisableMouse = "\x1b[?1000l" + RequestMouse = "\x1b[?1000$p" +) + +// VT Hilite Mouse Tracking is a mode that determines whether the mouse reports on +// button presses, releases, and highlighted cells. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +const ( + EnableMouseHilite = "\x1b[?1001h" + DisableMouseHilite = "\x1b[?1001l" + RequestMouseHilite = "\x1b[?1001$p" +) + +// Cell Motion Mouse Tracking is a mode that determines whether the mouse +// reports on button press, release, and motion events. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +const ( + EnableMouseCellMotion = "\x1b[?1002h" + DisableMouseCellMotion = "\x1b[?1002l" + RequestMouseCellMotion = "\x1b[?1002$p" +) + +// All Mouse Tracking is a mode that determines whether the mouse reports on +// button press, release, motion, and highlight events. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +const ( + EnableMouseAllMotion = "\x1b[?1003h" + DisableMouseAllMotion = "\x1b[?1003l" + RequestMouseAllMotion = "\x1b[?1003$p" +) + +// SGR Mouse Extension is a mode that determines whether the mouse reports events +// formatted with SGR parameters. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +const ( + EnableMouseSgrExt = "\x1b[?1006h" + DisableMouseSgrExt = "\x1b[?1006l" + RequestMouseSgrExt = "\x1b[?1006$p" +) + +// Alternate Screen Buffer is a mode that determines whether the alternate screen +// buffer is active. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer +const ( + EnableAltScreenBuffer = "\x1b[?1049h" + DisableAltScreenBuffer = "\x1b[?1049l" + RequestAltScreenBuffer = "\x1b[?1049$p" +) + +// Bracketed Paste Mode is a mode that determines whether pasted text is +// bracketed with escape sequences. +// +// See: https://cirw.in/blog/bracketed-paste +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Bracketed-Paste-Mode +const ( + EnableBracketedPaste = "\x1b[?2004h" + DisableBracketedPaste = "\x1b[?2004l" + RequestBracketedPaste = "\x1b[?2004$p" +) + +// Synchronized Output Mode is a mode that determines whether output is +// synchronized with the terminal. +// +// See: https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036 +const ( + EnableSyncdOutput = "\x1b[?2026h" + DisableSyncdOutput = "\x1b[?2026l" + RequestSyncdOutput = "\x1b[?2026$p" +) + +// Win32Input is a mode that determines whether input is processed by the +// Win32 console and Conpty. +// +// See: https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md +const ( + EnableWin32Input = "\x1b[?9001h" + DisableWin32Input = "\x1b[?9001l" + RequestWin32Input = "\x1b[?9001$p" +) diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/osc.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/osc.go new file mode 100644 index 000000000..40b543c29 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/osc.go @@ -0,0 +1,69 @@ +package ansi + +import ( + "bytes" + "strings" +) + +// OscSequence represents an OSC sequence. +// +// The sequence starts with a OSC sequence, OSC (0x9D) in a 8-bit environment +// or ESC ] (0x1B 0x5D) in a 7-bit environment, followed by positive integer identifier, +// then by arbitrary data terminated by a ST (0x9C) in a 8-bit environment, +// ESC \ (0x1B 0x5C) in a 7-bit environment, or BEL (0x07) for backwards compatibility. +// +// OSC Ps ; Pt ST +// OSC Ps ; Pt BEL +// +// See ECMA-48 § 5.7. +type OscSequence struct { + // Data contains the raw data of the sequence including the identifier + // command. + Data []byte + + // Cmd contains the raw command of the sequence. + Cmd int +} + +var _ Sequence = OscSequence{} + +// Command returns the command of the OSC sequence. +func (s OscSequence) Command() int { + return s.Cmd +} + +// Params returns the parameters of the OSC sequence split by ';'. +// The first element is the identifier command. +func (s OscSequence) Params() []string { + return strings.Split(string(s.Data), ";") +} + +// Clone returns a copy of the OSC sequence. +func (s OscSequence) Clone() Sequence { + return OscSequence{ + Data: append([]byte(nil), s.Data...), + Cmd: s.Cmd, + } +} + +// String returns the string representation of the OSC sequence. +// To be more compatible with different terminal, this will always return a +// 7-bit formatted sequence, terminated by BEL. +func (s OscSequence) String() string { + return s.buffer().String() +} + +// Bytes returns the byte representation of the OSC sequence. +// To be more compatible with different terminal, this will always return a +// 7-bit formatted sequence, terminated by BEL. +func (s OscSequence) Bytes() []byte { + return s.buffer().Bytes() +} + +func (s OscSequence) buffer() *bytes.Buffer { + var b bytes.Buffer + b.WriteString("\x1b]") + b.Write(s.Data) + b.WriteByte(BEL) + return &b +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/params.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/params.go new file mode 100644 index 000000000..a1bb42498 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/params.go @@ -0,0 +1,45 @@ +package ansi + +import ( + "bytes" +) + +// Params parses and returns a list of control sequence parameters. +// +// Parameters are positive integers separated by semicolons. Empty parameters +// default to zero. Parameters can have sub-parameters separated by colons. +// +// Any non-parameter bytes are ignored. This includes bytes that are not in the +// range of 0x30-0x3B. +// +// See ECMA-48 § 5.4.1. +func Params(p []byte) [][]uint { + if len(p) == 0 { + return [][]uint{} + } + + // Filter out non-parameter bytes i.e. non 0x30-0x3B. + p = bytes.TrimFunc(p, func(r rune) bool { + return r < 0x30 || r > 0x3B + }) + + parts := bytes.Split(p, []byte{';'}) + params := make([][]uint, len(parts)) + for i, part := range parts { + sparts := bytes.Split(part, []byte{':'}) + params[i] = make([]uint, len(sparts)) + for j, spart := range sparts { + params[i][j] = bytesToUint16(spart) + } + } + + return params +} + +func bytesToUint16(b []byte) uint { + var n uint + for _, c := range b { + n = n*10 + uint(c-'0') + } + return n +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/parser.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/parser.go new file mode 100644 index 000000000..f40278ee8 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/parser.go @@ -0,0 +1,357 @@ +package ansi + +import ( + "unicode/utf8" + + "github.com/charmbracelet/x/exp/term/ansi/parser" +) + +// ParserDispatcher is a function that dispatches a sequence. +type ParserDispatcher func(Sequence) + +// Parser represents a DEC ANSI compatible sequence parser. +// +// It uses a state machine to parse ANSI escape sequences and control +// characters. The parser is designed to be used with a terminal emulator or +// similar application that needs to parse ANSI escape sequences and control +// characters. +// See package [parser] for more information. +// +//go:generate go run ./gen.go +type Parser struct { + // Params contains the raw parameters of the sequence. + // These parameters used when constructing CSI and DCS sequences. + Params []int + + // Data contains the raw data of the sequence. + // These data used when constructing OSC, DCS, SOS, PM, and APC sequences. + Data []byte + + // DataLen keeps track of the length of the data buffer. + // If DataLen is -1, the data buffer is unlimited and will grow as needed. + // Otherwise, DataLen is limited by the size of the Data buffer. + DataLen int + + // ParamsLen keeps track of the number of parameters. + // This is limited by the size of the Params buffer. + ParamsLen int + + // Cmd contains the raw command along with the private marker and + // intermediate bytes of the sequence. + // The first lower byte contains the command byte, the next byte contains + // the private marker, and the next byte contains the intermediate byte. + Cmd int + + // RuneLen keeps track of the number of bytes collected for a UTF-8 rune. + RuneLen int + + // RuneBuf contains the bytes collected for a UTF-8 rune. + RuneBuf [utf8.MaxRune]byte + + // State is the current state of the parser. + State byte +} + +// NewParser returns a new parser with the given sizes allocated. +// If dataSize is zero, the underlying data buffer will be unlimited and will +// grow as needed. +func NewParser(paramsSize, dataSize int) *Parser { + s := &Parser{ + Params: make([]int, paramsSize), + Data: make([]byte, dataSize), + } + if dataSize <= 0 { + s.DataLen = -1 + } + return s +} + +// Reset resets the parser to its initial state. +func (p *Parser) Reset() { + p.clear() + p.State = parser.GroundState +} + +// clear clears the parser parameters and command. +func (p *Parser) clear() { + if len(p.Params) > 0 { + p.Params[0] = parser.MissingParam + } + p.ParamsLen = 0 + p.Cmd = 0 + p.RuneLen = 0 +} + +// StateName returns the name of the current state. +func (p *Parser) StateName() string { + return parser.StateNames[p.State] +} + +// Parse parses the given dispatcher and byte buffer. +func (p *Parser) Parse(dispatcher ParserDispatcher, b []byte) { + for i := 0; i < len(b); i++ { + p.Advance(dispatcher, b[i], i < len(b)-1) + } +} + +// Advance advances the parser with the given dispatcher and byte. +func (p *Parser) Advance(dispatcher ParserDispatcher, b byte, more bool) parser.Action { + switch p.State { + case parser.Utf8State: + // We handle UTF-8 here. + return p.advanceUtf8(dispatcher, b) + default: + return p.advance(dispatcher, b, more) + } +} + +func (p *Parser) collectRune(b byte) { + if p.RuneLen < utf8.UTFMax { + p.RuneBuf[p.RuneLen] = b + p.RuneLen++ + } +} + +func (p *Parser) advanceUtf8(dispatcher ParserDispatcher, b byte) parser.Action { + // Collect UTF-8 rune bytes. + p.collectRune(b) + rw := utf8ByteLen(p.RuneBuf[0]) + if rw == -1 { + // We panic here because the first byte comes from the state machine, + // if this panics, it means there is a bug in the state machine! + panic("invalid rune") // unreachable + } + + if p.RuneLen < rw { + return parser.NoneAction + } + + // We have enough bytes to decode the rune + bts := p.RuneBuf[:rw] + r, _ := utf8.DecodeRune(bts) + if dispatcher != nil { + dispatcher(Rune(r)) + } + + p.State = parser.GroundState + p.RuneLen = 0 + + return parser.NoneAction +} + +func (p *Parser) advance(d ParserDispatcher, b byte, more bool) parser.Action { + state, action := parser.Table.Transition(p.State, b) + + // We need to clear the parser state if the state changes from EscapeState. + // This is because when we enter the EscapeState, we don't get a chance to + // clear the parser state. For example, when a sequence terminates with a + // ST (\x1b\\ or \x9c), we dispatch the current sequence and transition to + // EscapeState. However, the parser state is not cleared in this case and + // we need to clear it here before dispatching the esc sequence. + if p.State != state { + switch p.State { + case parser.EscapeState: + p.performAction(d, parser.ClearAction, b) + } + if action == parser.PutAction && + p.State == parser.DcsEntryState && state == parser.DcsStringState { + // XXX: This is a special case where we need to start collecting + // non-string parameterized data i.e. doesn't follow the ECMA-48 § + // 5.4.1 string parameters format. + p.performAction(d, parser.StartAction, 0) + } + } + + // Handle special cases + switch { + case b == ESC && p.State == parser.EscapeState: + // Two ESCs in a row + p.performAction(d, parser.ExecuteAction, b) + if !more { + // Two ESCs at the end of the buffer + p.performAction(d, parser.ExecuteAction, b) + } + case b == ESC && !more: + // Last byte is an ESC + p.performAction(d, parser.ExecuteAction, b) + case p.State == parser.EscapeState && b == 'P' && !more: + // ESC P (DCS) at the end of the buffer + p.performAction(d, parser.DispatchAction, b) + case p.State == parser.EscapeState && b == 'X' && !more: + // ESC X (SOS) at the end of the buffer + p.performAction(d, parser.DispatchAction, b) + case p.State == parser.EscapeState && b == '[' && !more: + // ESC [ (CSI) at the end of the buffer + p.performAction(d, parser.DispatchAction, b) + case p.State == parser.EscapeState && b == ']' && !more: + // ESC ] (OSC) at the end of the buffer + p.performAction(d, parser.DispatchAction, b) + case p.State == parser.EscapeState && b == '^' && !more: + // ESC ^ (PM) at the end of the buffer + p.performAction(d, parser.DispatchAction, b) + case p.State == parser.EscapeState && b == '_' && !more: + // ESC _ (APC) at the end of the buffer + p.performAction(d, parser.DispatchAction, b) + default: + p.performAction(d, action, b) + } + + p.State = state + + return action +} + +func (p *Parser) performAction(dispatcher ParserDispatcher, action parser.Action, b byte) { + switch action { + case parser.IgnoreAction: + break + + case parser.ClearAction: + p.clear() + + case parser.PrintAction: + if utf8ByteLen(b) > 1 { + p.collectRune(b) + } else if dispatcher != nil { + dispatcher(Rune(b)) + } + + case parser.ExecuteAction: + if dispatcher != nil { + dispatcher(ControlCode(b)) + } + + case parser.MarkerAction: + // Collect private marker + // we only store the last marker + p.Cmd &^= 0xff << parser.MarkerShift + p.Cmd |= int(b) << parser.MarkerShift + + case parser.CollectAction: + // Collect intermediate bytes + // we only store the last intermediate byte + p.Cmd &^= 0xff << parser.IntermedShift + p.Cmd |= int(b) << parser.IntermedShift + + case parser.ParamAction: + // Collect parameters + if p.ParamsLen >= len(p.Params) { + break + } + + if b >= '0' && b <= '9' { + if p.Params[p.ParamsLen] == parser.MissingParam { + p.Params[p.ParamsLen] = 0 + } + + p.Params[p.ParamsLen] *= 10 + p.Params[p.ParamsLen] += int(b - '0') + } + + if b == ':' { + p.Params[p.ParamsLen] |= parser.HasMoreFlag + } + + if b == ';' || b == ':' { + p.ParamsLen++ + if p.ParamsLen < len(p.Params) { + p.Params[p.ParamsLen] = parser.MissingParam + } + } + + case parser.StartAction: + if p.DataLen < 0 { + p.Data = make([]byte, 0) + } else { + p.DataLen = 0 + } + if p.State >= parser.DcsEntryState && p.State <= parser.DcsStringState { + // Collect the command byte for DCS + p.Cmd |= int(b) + } else { + p.Cmd = parser.MissingCommand + } + + case parser.PutAction: + switch p.State { + case parser.OscStringState: + if b == ';' && p.Cmd == parser.MissingCommand { + // Try to parse the command + datalen := len(p.Data) + if p.DataLen >= 0 { + datalen = p.DataLen + } + for i := 0; i < datalen; i++ { + d := p.Data[i] + if d < '0' || d > '9' { + break + } + if p.Cmd == parser.MissingCommand { + p.Cmd = 0 + } + p.Cmd *= 10 + p.Cmd += int(d - '0') + } + } + } + + if p.DataLen < 0 { + p.Data = append(p.Data, b) + } else { + if p.DataLen < len(p.Data) { + p.Data[p.DataLen] = b + p.DataLen++ + } + } + + case parser.DispatchAction: + // Increment the last parameter + if p.ParamsLen > 0 && p.ParamsLen < len(p.Params)-1 || + p.ParamsLen == 0 && len(p.Params) > 0 && p.Params[0] != parser.MissingParam { + p.ParamsLen++ + } + + if dispatcher == nil { + break + } + + var seq Sequence + data := p.Data + if p.DataLen >= 0 { + data = data[:p.DataLen] + } + switch p.State { + case parser.CsiEntryState, parser.CsiParamState, parser.CsiIntermediateState: + p.Cmd |= int(b) + seq = CsiSequence{Cmd: p.Cmd, Params: p.Params[:p.ParamsLen]} + case parser.EscapeState, parser.EscapeIntermediateState: + p.Cmd |= int(b) + seq = EscSequence(p.Cmd) + case parser.DcsEntryState, parser.DcsParamState, parser.DcsIntermediateState, parser.DcsStringState: + seq = DcsSequence{Cmd: p.Cmd, Params: p.Params[:p.ParamsLen], Data: data} + case parser.OscStringState: + seq = OscSequence{Cmd: p.Cmd, Data: data} + case parser.SosStringState: + seq = SosSequence{Data: data} + case parser.PmStringState: + seq = PmSequence{Data: data} + case parser.ApcStringState: + seq = ApcSequence{Data: data} + } + + dispatcher(seq) + } +} + +func utf8ByteLen(b byte) int { + if b <= 0b0111_1111 { // 0x00-0x7F + return 1 + } else if b >= 0b1100_0000 && b <= 0b1101_1111 { // 0xC0-0xDF + return 2 + } else if b >= 0b1110_0000 && b <= 0b1110_1111 { // 0xE0-0xEF + return 3 + } else if b >= 0b1111_0000 && b <= 0b1111_0111 { // 0xF0-0xF7 + return 4 + } + return -1 +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/parser/const.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/parser/const.go new file mode 100644 index 000000000..54b7383b4 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/parser/const.go @@ -0,0 +1,78 @@ +package parser + +// Action is a DEC ANSI parser action. +type Action = byte + +// These are the actions that the parser can take. +const ( + NoneAction Action = iota + ClearAction + CollectAction + MarkerAction + DispatchAction + ExecuteAction + StartAction // Start of a data string + PutAction // Put into the data string + ParamAction + PrintAction + + IgnoreAction = NoneAction +) + +// nolint: unused +var ActionNames = []string{ + "NoneAction", + "ClearAction", + "CollectAction", + "MarkerAction", + "DispatchAction", + "ExecuteAction", + "StartAction", + "PutAction", + "ParamAction", + "PrintAction", +} + +// State is a DEC ANSI parser state. +type State = byte + +// These are the states that the parser can be in. +const ( + GroundState State = iota + CsiEntryState + CsiIntermediateState + CsiParamState + DcsEntryState + DcsIntermediateState + DcsParamState + DcsStringState + EscapeState + EscapeIntermediateState + OscStringState + SosStringState + PmStringState + ApcStringState + + // Utf8State is not part of the DEC ANSI standard. It is used to handle + // UTF-8 sequences. + Utf8State +) + +// nolint: unused +var StateNames = []string{ + "GroundState", + "CsiEntryState", + "CsiIntermediateState", + "CsiParamState", + "DcsEntryState", + "DcsIntermediateState", + "DcsParamState", + "DcsStringState", + "EscapeState", + "EscapeIntermediateState", + "OscStringState", + "SosStringState", + "PmStringState", + "ApcStringState", + "Utf8State", +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/parser/seq.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/parser/seq.go new file mode 100644 index 000000000..c99f1632f --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/parser/seq.go @@ -0,0 +1,136 @@ +package parser + +import "math" + +// Shift and masks for sequence parameters and intermediates. +const ( + MarkerShift = 8 + IntermedShift = 16 + CommandMask = 0xff + HasMoreFlag = math.MinInt32 + ParamMask = ^HasMoreFlag + MissingParam = ParamMask + MissingCommand = MissingParam + MaxParam = math.MaxUint16 // the maximum value a parameter can have +) + +const ( + // MaxParamsSize is the maximum number of parameters a sequence can have. + MaxParamsSize = 32 + + // DefaultParamValue is the default value used for missing parameters. + DefaultParamValue = 0 +) + +// Marker returns the marker byte of the sequence. +// This is always gonna be one of the following '<' '=' '>' '?' and in the +// range of 0x3C-0x3F. +// Zero is returned if the sequence does not have a marker. +func Marker(cmd int) int { + return (cmd >> MarkerShift) & CommandMask +} + +// Intermediate returns the intermediate byte of the sequence. +// An intermediate byte is in the range of 0x20-0x2F. This includes these +// characters from ' ', '!', '"', '#', '$', '%', '&', ”', '(', ')', '*', '+', +// ',', '-', '.', '/'. +// Zero is returned if the sequence does not have an intermediate byte. +func Intermediate(cmd int) int { + return (cmd >> IntermedShift) & CommandMask +} + +// Command returns the command byte of the CSI sequence. +func Command(cmd int) int { + return cmd & CommandMask +} + +// Param returns the parameter at the given index. +// It returns -1 if the parameter does not exist. +func Param(params []int, i int) int { + if len(params) == 0 || i < 0 || i >= len(params) { + return -1 + } + + p := params[i] & ParamMask + if p == MissingParam { + return -1 + } + + return p +} + +// HasMore returns true if the parameter has more sub-parameters. +func HasMore(params []int, i int) bool { + if len(params) == 0 || i >= len(params) { + return false + } + + return params[i]&HasMoreFlag != 0 +} + +// Subparams returns the sub-parameters of the given parameter. +// It returns nil if the parameter does not exist. +func Subparams(params []int, i int) []int { + if len(params) == 0 || i < 0 || i >= len(params) { + return nil + } + + // Count the number of parameters before the given parameter index. + var count int + var j int + for j = 0; j < len(params); j++ { + if count == i { + break + } + if !HasMore(params, j) { + count++ + } + } + + if count > i || j >= len(params) { + return nil + } + + var subs []int + for ; j < len(params); j++ { + if !HasMore(params, j) { + break + } + p := Param(params, j) + if p == -1 { + p = DefaultParamValue + } + subs = append(subs, p) + } + + p := Param(params, j) + if p == -1 { + p = DefaultParamValue + } + + return append(subs, p) +} + +// Len returns the number of parameters in the sequence. +// This will return the number of parameters in the sequence, excluding any +// sub-parameters. +func Len(params []int) int { + var n int + for i := 0; i < len(params); i++ { + if !HasMore(params, i) { + n++ + } + } + return n +} + +// Range iterates over the parameters of the sequence and calls the given +// function for each parameter. +// The function should return false to stop the iteration. +func Range(params []int, fn func(i int, param int, hasMore bool) bool) { + for i := 0; i < len(params); i++ { + if !fn(i, Param(params, i), HasMore(params, i)) { + break + } + } +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/parser/transition_table.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/parser/transition_table.go new file mode 100644 index 000000000..febde15dd --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/parser/transition_table.go @@ -0,0 +1,269 @@ +package parser + +// Table values are generated like this: +// +// index: currentState << IndexStateShift | charCode +// value: action << TransitionActionShift | nextState +const ( + TransitionActionShift = 4 + TransitionStateMask = 15 + IndexStateShift = 8 + + // DefaultTableSize is the default size of the transition table. + DefaultTableSize = 4096 +) + +// Table is a DEC ANSI transition table. +var Table = GenerateTransitionTable() + +// TransitionTable is a DEC ANSI transition table. +// https://vt100.net/emu/dec_ansi_parser +type TransitionTable []byte + +// NewTransitionTable returns a new DEC ANSI transition table. +func NewTransitionTable(size int) TransitionTable { + if size <= 0 { + size = DefaultTableSize + } + return TransitionTable(make([]byte, size)) +} + +// SetDefault sets default transition. +func (t TransitionTable) SetDefault(action Action, state State) { + for i := 0; i < len(t); i++ { + t[i] = action<> TransitionActionShift +} + +// byte range macro +func r(start, end byte) []byte { + var a []byte + for i := int(start); i <= int(end); i++ { + a = append(a, byte(i)) + } + return a +} + +// GenerateTransitionTable generates a DEC ANSI transition table compatible +// with the VT500-series of terminals. This implementation includes a few +// modifications that include: +// - A new Utf8State is introduced to handle UTF8 sequences. +// - Osc and Dcs data accept UTF8 sequences by extending the printable range +// to 0xFF and 0xFE respectively. +// - We don't ignore 0x3A (':') when building Csi and Dcs parameters and +// instead use it to denote sub-parameters. +// - Support dispatching SosPmApc sequences. +func GenerateTransitionTable() TransitionTable { + table := NewTransitionTable(DefaultTableSize) + table.SetDefault(NoneAction, GroundState) + + // Anywhere + for _, state := range r(GroundState, Utf8State) { + // Anywhere -> Ground + table.AddMany([]byte{0x18, 0x1a, 0x99, 0x9a}, state, ExecuteAction, GroundState) + table.AddRange(0x80, 0x8F, state, ExecuteAction, GroundState) + table.AddRange(0x90, 0x97, state, ExecuteAction, GroundState) + table.AddOne(0x9C, state, IgnoreAction, GroundState) + // Anywhere -> Escape + table.AddOne(0x1B, state, ClearAction, EscapeState) + // Anywhere -> SosStringState + table.AddOne(0x98, state, StartAction, SosStringState) + // Anywhere -> PmStringState + table.AddOne(0x9E, state, StartAction, PmStringState) + // Anywhere -> ApcStringState + table.AddOne(0x9F, state, StartAction, ApcStringState) + // Anywhere -> CsiEntry + table.AddOne(0x9B, state, ClearAction, CsiEntryState) + // Anywhere -> DcsEntry + table.AddOne(0x90, state, ClearAction, DcsEntryState) + // Anywhere -> OscString + table.AddOne(0x9D, state, StartAction, OscStringState) + // Anywhere -> Utf8 + table.AddRange(0xC2, 0xDF, state, PrintAction, Utf8State) // UTF8 2 byte sequence + table.AddRange(0xE0, 0xEF, state, PrintAction, Utf8State) // UTF8 3 byte sequence + table.AddRange(0xF0, 0xF4, state, PrintAction, Utf8State) // UTF8 4 byte sequence + } + + // Ground + table.AddRange(0x00, 0x17, GroundState, ExecuteAction, GroundState) + table.AddOne(0x19, GroundState, ExecuteAction, GroundState) + table.AddRange(0x1C, 0x1F, GroundState, ExecuteAction, GroundState) + table.AddRange(0x20, 0x7F, GroundState, PrintAction, GroundState) + + // EscapeIntermediate + table.AddRange(0x00, 0x17, EscapeIntermediateState, ExecuteAction, EscapeIntermediateState) + table.AddOne(0x19, EscapeIntermediateState, ExecuteAction, EscapeIntermediateState) + table.AddRange(0x1C, 0x1F, EscapeIntermediateState, ExecuteAction, EscapeIntermediateState) + table.AddRange(0x20, 0x2F, EscapeIntermediateState, CollectAction, EscapeIntermediateState) + table.AddOne(0x7F, EscapeIntermediateState, IgnoreAction, EscapeIntermediateState) + // EscapeIntermediate -> Ground + table.AddRange(0x30, 0x7E, EscapeIntermediateState, DispatchAction, GroundState) + + // Escape + table.AddRange(0x00, 0x17, EscapeState, ExecuteAction, EscapeState) + table.AddOne(0x19, EscapeState, ExecuteAction, EscapeState) + table.AddRange(0x1C, 0x1F, EscapeState, ExecuteAction, EscapeState) + table.AddOne(0x7F, EscapeState, IgnoreAction, EscapeState) + // Escape -> Ground + table.AddRange(0x30, 0x4F, EscapeState, DispatchAction, GroundState) + table.AddRange(0x51, 0x57, EscapeState, DispatchAction, GroundState) + table.AddOne(0x59, EscapeState, DispatchAction, GroundState) + table.AddOne(0x5A, EscapeState, DispatchAction, GroundState) + table.AddOne(0x5C, EscapeState, DispatchAction, GroundState) + table.AddRange(0x60, 0x7E, EscapeState, DispatchAction, GroundState) + // Escape -> Escape_intermediate + table.AddRange(0x20, 0x2F, EscapeState, CollectAction, EscapeIntermediateState) + // Escape -> Sos_pm_apc_string + table.AddOne('X', EscapeState, StartAction, SosStringState) // SOS + table.AddOne('^', EscapeState, StartAction, PmStringState) // PM + table.AddOne('_', EscapeState, StartAction, ApcStringState) // APC + // Escape -> Dcs_entry + table.AddOne('P', EscapeState, ClearAction, DcsEntryState) + // Escape -> Csi_entry + table.AddOne('[', EscapeState, ClearAction, CsiEntryState) + // Escape -> Osc_string + table.AddOne(']', EscapeState, StartAction, OscStringState) + + // Sos_pm_apc_string + for _, state := range r(SosStringState, ApcStringState) { + table.AddRange(0x00, 0x17, state, PutAction, state) + table.AddOne(0x19, state, PutAction, state) + table.AddRange(0x1C, 0x1F, state, PutAction, state) + table.AddRange(0x20, 0x7F, state, PutAction, state) + // ESC, ST, CAN, and SUB terminate the sequence + table.AddOne(0x1B, state, DispatchAction, EscapeState) + table.AddOne(0x9C, state, DispatchAction, GroundState) + table.AddMany([]byte{0x18, 0x1A}, state, IgnoreAction, GroundState) + } + + // Dcs_entry + table.AddRange(0x00, 0x07, DcsEntryState, IgnoreAction, DcsEntryState) + table.AddRange(0x0E, 0x17, DcsEntryState, IgnoreAction, DcsEntryState) + table.AddOne(0x19, DcsEntryState, IgnoreAction, DcsEntryState) + table.AddRange(0x1C, 0x1F, DcsEntryState, IgnoreAction, DcsEntryState) + table.AddOne(0x7F, DcsEntryState, IgnoreAction, DcsEntryState) + // Dcs_entry -> Dcs_intermediate + table.AddRange(0x20, 0x2F, DcsEntryState, CollectAction, DcsIntermediateState) + // Dcs_entry -> Dcs_param + table.AddRange(0x30, 0x3B, DcsEntryState, ParamAction, DcsParamState) + table.AddRange(0x3C, 0x3F, DcsEntryState, MarkerAction, DcsParamState) + // Dcs_entry -> Dcs_passthrough + table.AddRange(0x08, 0x0D, DcsEntryState, PutAction, DcsStringState) // Follows ECMA-48 § 8.3.27 + // XXX: allows passing ESC (not a ECMA-48 standard) this to allow for + // passthrough of ANSI sequences like in Screen or Tmux passthrough mode. + table.AddOne(0x1B, DcsEntryState, PutAction, DcsStringState) + table.AddRange(0x40, 0x7E, DcsEntryState, StartAction, DcsStringState) + + // Dcs_intermediate + table.AddRange(0x00, 0x17, DcsIntermediateState, IgnoreAction, DcsIntermediateState) + table.AddOne(0x19, DcsIntermediateState, IgnoreAction, DcsIntermediateState) + table.AddRange(0x1C, 0x1F, DcsIntermediateState, IgnoreAction, DcsIntermediateState) + table.AddRange(0x20, 0x2F, DcsIntermediateState, CollectAction, DcsIntermediateState) + table.AddOne(0x7F, DcsIntermediateState, IgnoreAction, DcsIntermediateState) + // Dcs_intermediate -> Dcs_passthrough + table.AddRange(0x30, 0x3F, DcsIntermediateState, StartAction, DcsStringState) + table.AddRange(0x40, 0x7E, DcsIntermediateState, StartAction, DcsStringState) + + // Dcs_param + table.AddRange(0x00, 0x17, DcsParamState, IgnoreAction, DcsParamState) + table.AddOne(0x19, DcsParamState, IgnoreAction, DcsParamState) + table.AddRange(0x1C, 0x1F, DcsParamState, IgnoreAction, DcsParamState) + table.AddRange(0x30, 0x3B, DcsParamState, ParamAction, DcsParamState) + table.AddOne(0x7F, DcsParamState, IgnoreAction, DcsParamState) + table.AddRange(0x3C, 0x3F, DcsParamState, IgnoreAction, DcsParamState) + // Dcs_param -> Dcs_intermediate + table.AddRange(0x20, 0x2F, DcsParamState, CollectAction, DcsIntermediateState) + // Dcs_param -> Dcs_passthrough + table.AddRange(0x40, 0x7E, DcsParamState, StartAction, DcsStringState) + + // Dcs_passthrough + table.AddRange(0x00, 0x17, DcsStringState, PutAction, DcsStringState) + table.AddOne(0x19, DcsStringState, PutAction, DcsStringState) + table.AddRange(0x1C, 0x1F, DcsStringState, PutAction, DcsStringState) + table.AddRange(0x20, 0x7E, DcsStringState, PutAction, DcsStringState) + table.AddOne(0x7F, DcsStringState, IgnoreAction, DcsStringState) + table.AddRange(0x80, 0xFF, DcsStringState, PutAction, DcsStringState) // Allow Utf8 characters by extending the printable range from 0x7F to 0xFF + // ST, CAN, SUB, and ESC terminate the sequence + table.AddOne(0x1B, DcsStringState, DispatchAction, EscapeState) + table.AddOne(0x9C, DcsStringState, DispatchAction, GroundState) + table.AddMany([]byte{0x18, 0x1A}, DcsStringState, IgnoreAction, GroundState) + + // Csi_param + table.AddRange(0x00, 0x17, CsiParamState, ExecuteAction, CsiParamState) + table.AddOne(0x19, CsiParamState, ExecuteAction, CsiParamState) + table.AddRange(0x1C, 0x1F, CsiParamState, ExecuteAction, CsiParamState) + table.AddRange(0x30, 0x3B, CsiParamState, ParamAction, CsiParamState) + table.AddOne(0x7F, CsiParamState, IgnoreAction, CsiParamState) + table.AddRange(0x3C, 0x3F, CsiParamState, IgnoreAction, CsiParamState) + // Csi_param -> Ground + table.AddRange(0x40, 0x7E, CsiParamState, DispatchAction, GroundState) + // Csi_param -> Csi_intermediate + table.AddRange(0x20, 0x2F, CsiParamState, CollectAction, CsiIntermediateState) + + // Csi_intermediate + table.AddRange(0x00, 0x17, CsiIntermediateState, ExecuteAction, CsiIntermediateState) + table.AddOne(0x19, CsiIntermediateState, ExecuteAction, CsiIntermediateState) + table.AddRange(0x1C, 0x1F, CsiIntermediateState, ExecuteAction, CsiIntermediateState) + table.AddRange(0x20, 0x2F, CsiIntermediateState, CollectAction, CsiIntermediateState) + table.AddOne(0x7F, CsiIntermediateState, IgnoreAction, CsiIntermediateState) + // Csi_intermediate -> Ground + table.AddRange(0x40, 0x7E, CsiIntermediateState, DispatchAction, GroundState) + // Csi_intermediate -> Csi_ignore + table.AddRange(0x30, 0x3F, CsiIntermediateState, IgnoreAction, GroundState) + + // Csi_entry + table.AddRange(0x00, 0x17, CsiEntryState, ExecuteAction, CsiEntryState) + table.AddOne(0x19, CsiEntryState, ExecuteAction, CsiEntryState) + table.AddRange(0x1C, 0x1F, CsiEntryState, ExecuteAction, CsiEntryState) + table.AddOne(0x7F, CsiEntryState, IgnoreAction, CsiEntryState) + // Csi_entry -> Ground + table.AddRange(0x40, 0x7E, CsiEntryState, DispatchAction, GroundState) + // Csi_entry -> Csi_intermediate + table.AddRange(0x20, 0x2F, CsiEntryState, CollectAction, CsiIntermediateState) + // Csi_entry -> Csi_param + table.AddRange(0x30, 0x3B, CsiEntryState, ParamAction, CsiParamState) + table.AddRange(0x3C, 0x3F, CsiEntryState, MarkerAction, CsiParamState) + + // Osc_string + table.AddRange(0x00, 0x06, OscStringState, IgnoreAction, OscStringState) + table.AddRange(0x08, 0x17, OscStringState, IgnoreAction, OscStringState) + table.AddOne(0x19, OscStringState, IgnoreAction, OscStringState) + table.AddRange(0x1C, 0x1F, OscStringState, IgnoreAction, OscStringState) + table.AddRange(0x20, 0xFF, OscStringState, PutAction, OscStringState) // Allow Utf8 characters by extending the printable range from 0x7F to 0xFF + + // ST, CAN, SUB, ESC, and BEL terminate the sequence + table.AddOne(0x1B, OscStringState, DispatchAction, EscapeState) + table.AddOne(0x07, OscStringState, DispatchAction, GroundState) + table.AddOne(0x9C, OscStringState, DispatchAction, GroundState) + table.AddMany([]byte{0x18, 0x1A}, OscStringState, IgnoreAction, GroundState) + + return table +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/passthrough.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/passthrough.go new file mode 100644 index 000000000..14a745220 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/passthrough.go @@ -0,0 +1,63 @@ +package ansi + +import ( + "bytes" +) + +// ScreenPassthrough wraps the given ANSI sequence in a DCS passthrough +// sequence to be sent to the outer terminal. This is used to send raw escape +// sequences to the outer terminal when running inside GNU Screen. +// +// DCS ST +// +// Note: Screen limits the length of string sequences to 768 bytes (since 2014). +// Use zero to indicate no limit, otherwise, this will chunk the returned +// string into limit sized chunks. +// +// See: https://www.gnu.org/software/screen/manual/screen.html#String-Escapes +// See: https://git.savannah.gnu.org/cgit/screen.git/tree/src/screen.h?id=c184c6ec27683ff1a860c45be5cf520d896fd2ef#n44 +func ScreenPassthrough(seq string, limit int) string { + var b bytes.Buffer + b.WriteString("\x1bP") + if limit > 0 { + for i := 0; i < len(seq); i += limit { + end := i + limit + if end > len(seq) { + end = len(seq) + } + b.WriteString(seq[i:end]) + if end < len(seq) { + b.WriteString("\x1b\\\x1bP") + } + } + } else { + b.WriteString(seq) + } + b.WriteString("\x1b\\") + return b.String() +} + +// TmuxPassthrough wraps the given ANSI sequence in a special DCS passthrough +// sequence to be sent to the outer terminal. This is used to send raw escape +// sequences to the outer terminal when running inside Tmux. +// +// DCS tmux ; ST +// +// Where is the given sequence in which all occurrences of ESC +// (0x1b) are doubled i.e. replaced with ESC ESC (0x1b 0x1b). +// +// Note: this needs the `allow-passthrough` option to be set to `on`. +// +// See: https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it +func TmuxPassthrough(seq string) string { + var b bytes.Buffer + b.WriteString("\x1bPtmux;") + for i := 0; i < len(seq); i++ { + if seq[i] == ESC { + b.WriteByte(ESC) + } + b.WriteByte(seq[i]) + } + b.WriteString("\x1b\\") + return b.String() +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/screen.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/screen.go new file mode 100644 index 000000000..4909cf07c --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/screen.go @@ -0,0 +1,126 @@ +package ansi + +import "strconv" + +// EraseDisplay (ED) clears the screen or parts of the screen. Possible values: +// +// 0: Clear from cursor to end of screen. +// 1: Clear from cursor to beginning of the screen. +// 2: Clear entire screen (and moves cursor to upper left on DOS). +// 3: Clear entire screen and delete all lines saved in the scrollback buffer. +// +// CSI J +// +// See: https://vt100.net/docs/vt510-rm/ED.html +func EraseDisplay(n int) string { + if n < 0 { + n = 0 + } + return "\x1b[" + strconv.Itoa(n) + "J" +} + +// EraseDisplay constants. +// These are the possible values for the EraseDisplay function. +const ( + EraseDisplayRight = "\x1b[0J" + EraseDisplayLeft = "\x1b[1J" + EraseEntireDisplay = "\x1b[2J" +) + +// EraseLine (EL) clears the current line or parts of the line. Possible values: +// +// 0: Clear from cursor to end of line. +// 1: Clear from cursor to beginning of the line. +// 2: Clear entire line. +// +// The cursor position is not affected. +// +// CSI K +// +// See: https://vt100.net/docs/vt510-rm/EL.html +func EraseLine(n int) string { + if n < 0 { + n = 0 + } + return "\x1b[" + strconv.Itoa(n) + "K" +} + +// EraseLine constants. +// These are the possible values for the EraseLine function. +const ( + EraseLineRight = "\x1b[0K" + EraseLineLeft = "\x1b[1K" + EraseEntireLine = "\x1b[2K" +) + +// ScrollUp (SU) scrolls the screen up n lines. New lines are added at the +// bottom of the screen. +// +// CSI S +// +// See: https://vt100.net/docs/vt510-rm/SU.html +func ScrollUp(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "S" +} + +// ScrollDown (SD) scrolls the screen down n lines. New lines are added at the +// top of the screen. +// +// CSI T +// +// See: https://vt100.net/docs/vt510-rm/SD.html +func ScrollDown(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "T" +} + +// InsertLine (IL) inserts n blank lines at the current cursor position. +// Existing lines are moved down. +// +// CSI L +// +// See: https://vt100.net/docs/vt510-rm/IL.html +func InsertLine(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "L" +} + +// DeleteLine (DL) deletes n lines at the current cursor position. Existing +// lines are moved up. +// +// CSI M +// +// See: https://vt100.net/docs/vt510-rm/DL.html +func DeleteLine(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "M" +} + +// SetScrollingRegion (DECSTBM) sets the top and bottom margins for the scrolling +// region. The default is the entire screen. +// +// CSI ; r +// +// See: https://vt100.net/docs/vt510-rm/DECSTBM.html +func SetScrollingRegion(t, b int) string { + if t < 0 { + t = 0 + } + if b < 0 { + b = 0 + } + return "\x1b[" + strconv.Itoa(t) + ";" + strconv.Itoa(b) + "r" +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/sequence.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/sequence.go new file mode 100644 index 000000000..86bbc3564 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/sequence.go @@ -0,0 +1,199 @@ +package ansi + +import ( + "bytes" + + "github.com/charmbracelet/x/exp/term/ansi/parser" +) + +// Sequence represents an ANSI sequence. This can be a control sequence, escape +// sequence, a printable character, etc. +type Sequence interface { + // String returns the string representation of the sequence. + String() string + // Bytes returns the byte representation of the sequence. + Bytes() []byte + // Clone returns a copy of the sequence. + Clone() Sequence +} + +// Rune represents a printable character. +type Rune rune + +var _ Sequence = Rune(0) + +// Bytes implements Sequence. +func (r Rune) Bytes() []byte { + return []byte(string(r)) +} + +// String implements Sequence. +func (r Rune) String() string { + return string(r) +} + +// Clone implements Sequence. +func (r Rune) Clone() Sequence { + return r +} + +// ControlCode represents a control code character. This is a character that +// is not printable and is used to control the terminal. This would be a +// character in the C0 or C1 set in the range of 0x00-0x1F and 0x80-0x9F. +type ControlCode byte + +var _ Sequence = ControlCode(0) + +// Bytes implements Sequence. +func (c ControlCode) Bytes() []byte { + return []byte{byte(c)} +} + +// String implements Sequence. +func (c ControlCode) String() string { + return string(c) +} + +// Clone implements Sequence. +func (c ControlCode) Clone() Sequence { + return c +} + +// EscSequence represents an escape sequence. +type EscSequence int + +var _ Sequence = EscSequence(0) + +// buffer returns the buffer of the escape sequence. +func (e EscSequence) buffer() *bytes.Buffer { + var b bytes.Buffer + b.WriteByte('\x1b') + if i := parser.Intermediate(int(e)); i != 0 { + b.WriteByte(byte(i)) + } + b.WriteByte(byte(e.Command())) + return &b +} + +// Bytes implements Sequence. +func (e EscSequence) Bytes() []byte { + return e.buffer().Bytes() +} + +// String implements Sequence. +func (e EscSequence) String() string { + return e.buffer().String() +} + +// Clone implements Sequence. +func (e EscSequence) Clone() Sequence { + return e +} + +// Command returns the command byte of the escape sequence. +func (e EscSequence) Command() int { + return parser.Command(int(e)) +} + +// Intermediate returns the intermediate byte of the escape sequence. +func (e EscSequence) Intermediate() int { + return parser.Intermediate(int(e)) +} + +// SosSequence represents a SOS sequence. +type SosSequence struct { + // Data contains the raw data of the sequence. + Data []byte +} + +var _ Sequence = &SosSequence{} + +// Clone implements Sequence. +func (s SosSequence) Clone() Sequence { + return SosSequence{Data: append([]byte(nil), s.Data...)} +} + +// Bytes implements Sequence. +func (s SosSequence) Bytes() []byte { + return s.buffer().Bytes() +} + +// String implements Sequence. +func (s SosSequence) String() string { + return s.buffer().String() +} + +func (s SosSequence) buffer() *bytes.Buffer { + var b bytes.Buffer + b.WriteByte('\x1b') + b.WriteByte('X') + b.Write(s.Data) + b.WriteString("\x1b\\") + return &b +} + +// PmSequence represents a PM sequence. +type PmSequence struct { + // Data contains the raw data of the sequence. + Data []byte +} + +var _ Sequence = &PmSequence{} + +// Clone implements Sequence. +func (s PmSequence) Clone() Sequence { + return PmSequence{Data: append([]byte(nil), s.Data...)} +} + +// Bytes implements Sequence. +func (s PmSequence) Bytes() []byte { + return s.buffer().Bytes() +} + +// String implements Sequence. +func (s PmSequence) String() string { + return s.buffer().String() +} + +// buffer returns the buffer of the PM sequence. +func (s PmSequence) buffer() *bytes.Buffer { + var b bytes.Buffer + b.WriteByte('\x1b') + b.WriteByte('^') + b.Write(s.Data) + b.WriteString("\x1b\\") + return &b +} + +// ApcSequence represents an APC sequence. +type ApcSequence struct { + // Data contains the raw data of the sequence. + Data []byte +} + +var _ Sequence = &ApcSequence{} + +// Clone implements Sequence. +func (s ApcSequence) Clone() Sequence { + return ApcSequence{Data: append([]byte(nil), s.Data...)} +} + +// Bytes implements Sequence. +func (s ApcSequence) Bytes() []byte { + return s.buffer().Bytes() +} + +// String implements Sequence. +func (s ApcSequence) String() string { + return s.buffer().String() +} + +// buffer returns the buffer of the APC sequence. +func (s ApcSequence) buffer() *bytes.Buffer { + var b bytes.Buffer + b.WriteByte('\x1b') + b.WriteByte('_') + b.Write(s.Data) + b.WriteString("\x1b\\") + return &b +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/style.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/style.go new file mode 100644 index 000000000..33421850d --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/style.go @@ -0,0 +1,290 @@ +package ansi + +import ( + "image/color" + "strconv" + "strings" +) + +// ResetStyle is a SGR (Select Graphic Rendition) style sequence that resets +// all attributes. +// See: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters +const ResetStyle = "\x1b[m" + +// Attr is a SGR (Select Graphic Rendition) style attribute. +type Attr = string + +// Style represents an ANSI SGR (Select Graphic Rendition) style. +type Style []Attr + +// String returns the ANSI SGR (Select Graphic Rendition) style sequence for +// the given style. +func (s Style) String() string { + if len(s) == 0 { + return ResetStyle + } + return "\x1b[" + strings.Join(s, ";") + "m" +} + +// Styled returns a styled string with the given style applied. +func (s Style) Styled(str string) string { + if len(s) == 0 { + return str + } + return s.String() + str + ResetStyle +} + +// Reset appends the reset style attribute to the style. +func (s Style) Reset() Style { + return append(s, ResetAttr) +} + +// Bold appends the bold style attribute to the style. +func (s Style) Bold() Style { + return append(s, BoldAttr) +} + +// Faint appends the faint style attribute to the style. +func (s Style) Faint() Style { + return append(s, FaintAttr) +} + +// Italic appends the italic style attribute to the style. +func (s Style) Italic() Style { + return append(s, ItalicAttr) +} + +// Underline appends the underline style attribute to the style. +func (s Style) Underline() Style { + return append(s, UnderlineAttr) +} + +// DoubleUnderline appends the double underline style attribute to the style. +func (s Style) DoubleUnderline() Style { + return append(s, DoubleUnderlineAttr) +} + +// CurlyUnderline appends the curly underline style attribute to the style. +func (s Style) CurlyUnderline() Style { + return append(s, CurlyUnderlineAttr) +} + +// DottedUnderline appends the dotted underline style attribute to the style. +func (s Style) DottedUnderline() Style { + return append(s, DottedUnderlineAttr) +} + +// DashedUnderline appends the dashed underline style attribute to the style. +func (s Style) DashedUnderline() Style { + return append(s, DashedUnderlineAttr) +} + +// SlowBlink appends the slow blink style attribute to the style. +func (s Style) SlowBlink() Style { + return append(s, SlowBlinkAttr) +} + +// RapidBlink appends the rapid blink style attribute to the style. +func (s Style) RapidBlink() Style { + return append(s, RapidBlinkAttr) +} + +// Reverse appends the reverse style attribute to the style. +func (s Style) Reverse() Style { + return append(s, ReverseAttr) +} + +// Conceal appends the conceal style attribute to the style. +func (s Style) Conceal() Style { + return append(s, ConcealAttr) +} + +// Strikethrough appends the strikethrough style attribute to the style. +func (s Style) Strikethrough() Style { + return append(s, StrikethroughAttr) +} + +// NoBold appends the no bold style attribute to the style. +func (s Style) NoBold() Style { + return append(s, NoBoldAttr) +} + +// NormalIntensity appends the normal intensity style attribute to the style. +func (s Style) NormalIntensity() Style { + return append(s, NormalIntensityAttr) +} + +// NoItalic appends the no italic style attribute to the style. +func (s Style) NoItalic() Style { + return append(s, NoItalicAttr) +} + +// NoUnderline appends the no underline style attribute to the style. +func (s Style) NoUnderline() Style { + return append(s, NoUnderlineAttr) +} + +// NoBlink appends the no blink style attribute to the style. +func (s Style) NoBlink() Style { + return append(s, NoBlinkAttr) +} + +// NoReverse appends the no reverse style attribute to the style. +func (s Style) NoReverse() Style { + return append(s, NoReverseAttr) +} + +// NoStrikethrough appends the no strikethrough style attribute to the style. +func (s Style) NoStrikethrough() Style { + return append(s, NoStrikethroughAttr) +} + +// DefaultForegroundColor appends the default foreground color style attribute to the style. +func (s Style) DefaultForegroundColor() Style { + return append(s, DefaultForegroundColorAttr) +} + +// DefaultBackgroundColor appends the default background color style attribute to the style. +func (s Style) DefaultBackgroundColor() Style { + return append(s, DefaultBackgroundColorAttr) +} + +// DefaultUnderlineColor appends the default underline color style attribute to the style. +func (s Style) DefaultUnderlineColor() Style { + return append(s, DefaultUnderlineColorAttr) +} + +// ForegroundColor appends the foreground color style attribute to the style. +func (s Style) ForegroundColor(c Color) Style { + return append(s, ForegroundColorAttr(c)) +} + +// BackgroundColor appends the background color style attribute to the style. +func (s Style) BackgroundColor(c Color) Style { + return append(s, BackgroundColorAttr(c)) +} + +// UnderlineColor appends the underline color style attribute to the style. +func (s Style) UnderlineColor(c Color) Style { + return append(s, UnderlineColorAttr(c)) +} + +// SGR (Select Graphic Rendition) style attributes. +// See: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters +const ( + ResetAttr Attr = "0" + BoldAttr Attr = "1" + FaintAttr Attr = "2" + ItalicAttr Attr = "3" + UnderlineAttr Attr = "4" + DoubleUnderlineAttr Attr = "4:2" + CurlyUnderlineAttr Attr = "4:3" + DottedUnderlineAttr Attr = "4:4" + DashedUnderlineAttr Attr = "4:5" + SlowBlinkAttr Attr = "5" + RapidBlinkAttr Attr = "6" + ReverseAttr Attr = "7" + ConcealAttr Attr = "8" + StrikethroughAttr Attr = "9" + NoBoldAttr Attr = "21" // Some terminals treat this as double underline. + NormalIntensityAttr Attr = "22" + NoItalicAttr Attr = "23" + NoUnderlineAttr Attr = "24" + NoBlinkAttr Attr = "25" + NoReverseAttr Attr = "27" + NoStrikethroughAttr Attr = "29" + DefaultForegroundColorAttr Attr = "39" + DefaultBackgroundColorAttr Attr = "49" + DefaultUnderlineColorAttr Attr = "59" +) + +// ForegroundColorAttr returns the style SGR attribute for the given foreground +// color. +// See: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters +func ForegroundColorAttr(c Color) Attr { + switch c := c.(type) { + case BasicColor: + // 3-bit or 4-bit ANSI foreground + // "3" or "9" where n is the color number from 0 to 7 + if c < 8 { + return "3" + string('0'+c) + } else if c < 16 { + return "9" + string('0'+c-8) + } + case ExtendedColor: + // 256-color ANSI foreground + // "38;5;" + return "38;5;" + strconv.FormatUint(uint64(c), 10) + case TrueColor, color.Color: + // 24-bit "true color" foreground + // "38;2;;;" + r, g, b, _ := c.RGBA() + return "38;2;" + + strconv.FormatUint(uint64(shift(r)), 10) + ";" + + strconv.FormatUint(uint64(shift(g)), 10) + ";" + + strconv.FormatUint(uint64(shift(b)), 10) + } + return DefaultForegroundColorAttr +} + +// BackgroundColorAttr returns the style SGR attribute for the given background +// color. +// See: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters +func BackgroundColorAttr(c Color) Attr { + switch c := c.(type) { + case BasicColor: + // 3-bit or 4-bit ANSI foreground + // "4" or "10" where n is the color number from 0 to 7 + if c < 8 { + return "4" + string('0'+c) + } else { + return "10" + string('0'+c-8) + } + case ExtendedColor: + // 256-color ANSI foreground + // "48;5;" + return "48;5;" + strconv.FormatUint(uint64(c), 10) + case TrueColor, color.Color: + // 24-bit "true color" foreground + // "38;2;;;" + r, g, b, _ := c.RGBA() + return "48;2;" + + strconv.FormatUint(uint64(shift(r)), 10) + ";" + + strconv.FormatUint(uint64(shift(g)), 10) + ";" + + strconv.FormatUint(uint64(shift(b)), 10) + } + return DefaultBackgroundColorAttr +} + +// UnderlineColorAttr returns the style SGR attribute for the given underline +// color. +// See: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters +func UnderlineColorAttr(c Color) Attr { + switch c := c.(type) { + // NOTE: we can't use 3-bit and 4-bit ANSI color codes with underline + // color, use 256-color instead. + // + // 256-color ANSI underline color + // "58;5;" + case BasicColor: + return "58;5;" + strconv.FormatUint(uint64(c), 10) + case ExtendedColor: + return "58;5;" + strconv.FormatUint(uint64(c), 10) + case TrueColor, color.Color: + // 24-bit "true color" foreground + // "38;2;;;" + r, g, b, _ := c.RGBA() + return "58;2;" + + strconv.FormatUint(uint64(shift(r)), 10) + ";" + + strconv.FormatUint(uint64(shift(g)), 10) + ";" + + strconv.FormatUint(uint64(shift(b)), 10) + } + return DefaultUnderlineColorAttr +} + +func shift(v uint32) uint32 { + if v > 0xff { + return v >> 8 + } + return v +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/termcap.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/termcap.go new file mode 100644 index 000000000..1dfc52a6e --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/termcap.go @@ -0,0 +1,31 @@ +package ansi + +import ( + "encoding/hex" + "strings" +) + +// RequestTermcap (XTGETTCAP) requests Termcap/Terminfo strings. +// +// DCS + q ST +// +// Where is a list of Termcap/Terminfo capabilities, encoded in 2-digit +// hexadecimals, separated by semicolons. +// +// See: https://man7.org/linux/man-pages/man5/terminfo.5.html +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +func RequestTermcap(caps ...string) string { + if len(caps) == 0 { + return "" + } + + s := "\x1bP+q" + for i, c := range caps { + if i > 0 { + s += ";" + } + s += strings.ToUpper(hex.EncodeToString([]byte(c))) + } + + return s + "\x1b\\" +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/title.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/title.go new file mode 100644 index 000000000..8fd8bf98d --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/title.go @@ -0,0 +1,32 @@ +package ansi + +// SetIconNameWindowTitle returns a sequence for setting the icon name and +// window title. +// +// OSC 0 ; title ST +// OSC 0 ; title BEL +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands +func SetIconNameWindowTitle(s string) string { + return "\x1b]0;" + s + "\x07" +} + +// SetIconName returns a sequence for setting the icon name. +// +// OSC 1 ; title ST +// OSC 1 ; title BEL +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands +func SetIconName(s string) string { + return "\x1b]1;" + s + "\x07" +} + +// SetWindowTitle returns a sequence for setting the window title. +// +// OSC 2 ; title ST +// OSC 2 ; title BEL +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands +func SetWindowTitle(s string) string { + return "\x1b]2;" + s + "\x07" +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/truncate.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/truncate.go new file mode 100644 index 000000000..8d107c0a2 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/truncate.go @@ -0,0 +1,108 @@ +package ansi + +import ( + "bytes" + + "github.com/charmbracelet/x/exp/term/ansi/parser" + "github.com/rivo/uniseg" +) + +// Truncate truncates a string to a given length, adding a tail to the +// end if the string is longer than the given length. +// This function is aware of ANSI escape codes and will not break them, and +// accounts for wide-characters (such as East Asians and emojis). +func Truncate(s string, length int, tail string) string { + tw := StringWidth(tail) + length -= tw + if length < 0 { + return "" + } + + var cluster []byte + var buf bytes.Buffer + curWidth := 0 + ignoring := false + gstate := -1 + pstate := parser.GroundState // initial state + b := []byte(s) + i := 0 + + // Here we iterate over the bytes of the string and collect printable + // characters and runes. We also keep track of the width of the string + // in cells. + // Once we reach the given length, we start ignoring characters and only + // collect ANSI escape codes until we reach the end of string. + for i < len(b) { + state, action := parser.Table.Transition(pstate, b[i]) + + switch action { + case parser.PrintAction: + if utf8ByteLen(b[i]) > 1 { + // This action happens when we transition to the Utf8State. + var width int + cluster, _, width, gstate = uniseg.FirstGraphemeCluster(b[i:], gstate) + + // increment the index by the length of the cluster + i += len(cluster) + + // Are we ignoring? Skip to the next byte + if ignoring { + continue + } + + // Is this gonna be too wide? + // If so write the tail and stop collecting. + if curWidth+width > length && !ignoring { + ignoring = true + buf.WriteString(tail) + } + + if curWidth+width > length { + continue + } + + curWidth += width + for _, r := range cluster { + buf.WriteByte(r) + } + + gstate = -1 // reset grapheme state otherwise, width calculation might be off + // Done collecting, now we're back in the ground state. + pstate = parser.GroundState + continue + } + + // Is this gonna be too wide? + // If so write the tail and stop collecting. + if curWidth >= length && !ignoring { + ignoring = true + buf.WriteString(tail) + } + + // Skip to the next byte if we're ignoring + if ignoring { + i++ + continue + } + + // collects printable ASCII + curWidth++ + fallthrough + default: + buf.WriteByte(b[i]) + i++ + } + + // Transition to the next state. + pstate = state + + // Once we reach the given length, we start ignoring runes and write + // the tail to the buffer. + if curWidth > length && !ignoring { + ignoring = true + buf.WriteString(tail) + } + } + + return buf.String() +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/util.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/util.go new file mode 100644 index 000000000..767093f92 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/util.go @@ -0,0 +1,29 @@ +package ansi + +import ( + "fmt" + "image/color" +) + +// colorToHexString returns a hex string representation of a color. +func colorToHexString(c color.Color) string { + if c == nil { + return "" + } + shift := func(v uint32) uint32 { + if v > 0xff { + return v >> 8 + } + return v + } + r, g, b, _ := c.RGBA() + r, g, b = shift(r), shift(g), shift(b) + return fmt.Sprintf("#%02x%02x%02x", r, g, b) +} + +// rgbToHex converts red, green, and blue values to a hexadecimal value. +// +// hex := rgbToHex(0, 0, 255) // 0x0000FF +func rgbToHex(r, g, b uint32) uint32 { + return r<<16 + g<<8 + b +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/width.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/width.go new file mode 100644 index 000000000..71ce3b683 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/width.go @@ -0,0 +1,73 @@ +package ansi + +import ( + "bytes" + + "github.com/charmbracelet/x/exp/term/ansi/parser" + "github.com/rivo/uniseg" +) + +// Strip removes ANSI escape codes from a string. +func Strip(s string) string { + var ( + buf bytes.Buffer // buffer for collecting printable characters + ri int // rune index + rw int // rune width + pstate = parser.GroundState // initial state + ) + + // This implements a subset of the Parser to only collect runes and + // printable characters. + for i := 0; i < len(s); i++ { + var state, action byte + if pstate != parser.Utf8State { + state, action = parser.Table.Transition(pstate, s[i]) + } + + switch { + case pstate == parser.Utf8State: + // During this state, collect rw bytes to form a valid rune in the + // buffer. After getting all the rune bytes into the buffer, + // transition to GroundState and reset the counters. + buf.WriteByte(s[i]) + ri++ + if ri < rw { + continue + } + pstate = parser.GroundState + ri = 0 + rw = 0 + case action == parser.PrintAction: + // This action happens when we transition to the Utf8State. + if w := utf8ByteLen(s[i]); w > 1 { + rw = w + buf.WriteByte(s[i]) + ri++ + break + } + fallthrough + case action == parser.ExecuteAction: + // collects printable ASCII and non-printable characters + buf.WriteByte(s[i]) + } + + // Transition to the next state. + // The Utf8State is managed separately above. + if pstate != parser.Utf8State { + pstate = state + } + } + + return buf.String() +} + +// StringWidth returns the width of a string in cells. This is the number of +// cells that the string will occupy when printed in a terminal. ANSI escape +// codes are ignored and wide characters (such as East Asians and emojis) are +// accounted for. +func StringWidth(s string) int { + if s == "" { + return 0 + } + return uniseg.StringWidth(Strip(s)) +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/wrap.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/wrap.go new file mode 100644 index 000000000..5413d0c64 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/wrap.go @@ -0,0 +1,406 @@ +package ansi + +import ( + "bytes" + "unicode" + "unicode/utf8" + + "github.com/charmbracelet/x/exp/term/ansi/parser" + "github.com/rivo/uniseg" +) + +// nbsp is a non-breaking space +const nbsp = 0xA0 + +// Hardwrap wraps a string or a block of text to a given line length, breaking +// word boundaries. This will preserve ANSI escape codes and will account for +// wide-characters in the string. +// When preserveSpace is true, spaces at the beginning of a line will be +// preserved. +func Hardwrap(s string, limit int, preserveSpace bool) string { + if limit < 1 { + return s + } + + var ( + cluster []byte + buf bytes.Buffer + curWidth int + forceNewline bool + gstate = -1 + pstate = parser.GroundState // initial state + b = []byte(s) + ) + + addNewline := func() { + buf.WriteByte('\n') + curWidth = 0 + } + + i := 0 + for i < len(b) { + state, action := parser.Table.Transition(pstate, b[i]) + + switch action { + case parser.PrintAction: + if utf8ByteLen(b[i]) > 1 { + var width int + cluster, _, width, gstate = uniseg.FirstGraphemeCluster(b[i:], gstate) + i += len(cluster) + + if curWidth+width > limit { + addNewline() + } + if !preserveSpace && curWidth == 0 && len(cluster) <= 4 { + // Skip spaces at the beginning of a line + if r, _ := utf8.DecodeRune(cluster); r != utf8.RuneError && unicode.IsSpace(r) { + pstate = parser.GroundState + continue + } + } + + buf.Write(cluster) + curWidth += width + gstate = -1 // reset grapheme state otherwise, width calculation might be off + pstate = parser.GroundState + continue + } + fallthrough + case parser.ExecuteAction: + if b[i] == '\n' { + addNewline() + forceNewline = false + break + } + + if curWidth+1 > limit { + addNewline() + forceNewline = true + } + + // Skip spaces at the beginning of a line + if curWidth == 0 { + if !preserveSpace && forceNewline && unicode.IsSpace(rune(b[i])) { + break + } + forceNewline = false + } + + buf.WriteByte(b[i]) + curWidth++ + default: + buf.WriteByte(b[i]) + } + + // We manage the UTF8 state separately manually above. + if pstate != parser.Utf8State { + pstate = state + } + i++ + } + + return buf.String() +} + +// Wordwrap wraps a string or a block of text to a given line length, not +// breaking word boundaries. This will preserve ANSI escape codes and will +// account for wide-characters in the string. +// The breakpoints string is a list of characters that are considered +// breakpoints for word wrapping. A hyphen (-) is always considered a +// breakpoint. +// +// Note: breakpoints must be a string of 1-cell wide rune characters. +func Wordwrap(s string, limit int, breakpoints string) string { + if limit < 1 { + return s + } + + var ( + cluster []byte + buf bytes.Buffer + word bytes.Buffer + space bytes.Buffer + curWidth int + wordLen int + gstate = -1 + pstate = parser.GroundState // initial state + b = []byte(s) + ) + + addSpace := func() { + curWidth += space.Len() + buf.Write(space.Bytes()) + space.Reset() + } + + addWord := func() { + if word.Len() == 0 { + return + } + + addSpace() + curWidth += wordLen + buf.Write(word.Bytes()) + word.Reset() + wordLen = 0 + } + + addNewline := func() { + buf.WriteByte('\n') + curWidth = 0 + space.Reset() + } + + i := 0 + for i < len(b) { + state, action := parser.Table.Transition(pstate, b[i]) + + switch action { + case parser.PrintAction: + if utf8ByteLen(b[i]) > 1 { + var width int + cluster, _, width, gstate = uniseg.FirstGraphemeCluster(b[i:], gstate) + i += len(cluster) + + r, _ := utf8.DecodeRune(cluster) + if r != utf8.RuneError && unicode.IsSpace(r) && r != nbsp { + addWord() + space.WriteRune(r) + } else if bytes.ContainsAny(cluster, breakpoints) { + addSpace() + addWord() + buf.Write(cluster) + curWidth++ + } else { + word.Write(cluster) + wordLen += width + if curWidth+space.Len()+wordLen > limit && + wordLen < limit { + addNewline() + } + } + + pstate = parser.GroundState + continue + } + fallthrough + case parser.ExecuteAction: + r := rune(b[i]) + switch { + case r == '\n': + if wordLen == 0 { + if curWidth+space.Len() > limit { + curWidth = 0 + } else { + buf.Write(space.Bytes()) + } + space.Reset() + } + + addWord() + addNewline() + case unicode.IsSpace(r): + addWord() + space.WriteByte(b[i]) + case r == '-': + fallthrough + case runeContainsAny(r, breakpoints): + addSpace() + addWord() + buf.WriteByte(b[i]) + curWidth++ + default: + word.WriteByte(b[i]) + wordLen++ + if curWidth+space.Len()+wordLen > limit && + wordLen < limit { + addNewline() + } + } + + default: + word.WriteByte(b[i]) + } + + // We manage the UTF8 state separately manually above. + if pstate != parser.Utf8State { + pstate = state + } + i++ + } + + addWord() + + return buf.String() +} + +// Wrap wraps a string or a block of text to a given line length, breaking word +// boundaries if necessary. This will preserve ANSI escape codes and will +// account for wide-characters in the string. The breakpoints string is a list +// of characters that are considered breakpoints for word wrapping. A hyphen +// (-) is always considered a breakpoint. +// +// Note: breakpoints must be a string of 1-cell wide rune characters. +func Wrap(s string, limit int, breakpoints string) string { + if limit < 1 { + return s + } + + var ( + cluster []byte + buf bytes.Buffer + word bytes.Buffer + space bytes.Buffer + curWidth int // written width of the line + wordLen int // word buffer len without ANSI escape codes + gstate = -1 + pstate = parser.GroundState // initial state + b = []byte(s) + ) + + addSpace := func() { + curWidth += space.Len() + buf.Write(space.Bytes()) + space.Reset() + } + + addWord := func() { + if word.Len() == 0 { + return + } + + addSpace() + curWidth += wordLen + buf.Write(word.Bytes()) + word.Reset() + wordLen = 0 + } + + addNewline := func() { + buf.WriteByte('\n') + curWidth = 0 + space.Reset() + } + + i := 0 + for i < len(b) { + state, action := parser.Table.Transition(pstate, b[i]) + + switch action { + case parser.PrintAction: + if utf8ByteLen(b[i]) > 1 { + var width int + cluster, _, width, gstate = uniseg.FirstGraphemeCluster(b[i:], gstate) + i += len(cluster) + + r, _ := utf8.DecodeRune(cluster) + switch { + case r != utf8.RuneError && unicode.IsSpace(r) && r != nbsp: // nbsp is a non-breaking space + addWord() + space.WriteRune(r) + case bytes.ContainsAny(cluster, breakpoints): + addSpace() + if curWidth+wordLen+width > limit { + word.Write(cluster) + wordLen += width + } else { + addWord() + buf.Write(cluster) + curWidth += width + } + default: + if wordLen+width > limit { + // Hardwrap the word if it's too long + addWord() + } + + word.Write(cluster) + wordLen += width + + if curWidth+wordLen+space.Len() > limit { + addNewline() + } + } + + pstate = parser.GroundState + continue + } + + fallthrough + case parser.ExecuteAction: + switch r := rune(b[i]); { + case r == '\n': + if wordLen == 0 { + if curWidth+space.Len() > limit { + curWidth = 0 + } else { + // preserve whitespaces + buf.Write(space.Bytes()) + } + space.Reset() + } + + addWord() + addNewline() + case unicode.IsSpace(r): + addWord() + space.WriteRune(r) + case r == '-': + fallthrough + case runeContainsAny(r, breakpoints): + addSpace() + if curWidth+wordLen >= limit { + // We can't fit the breakpoint in the current line, treat + // it as part of the word. + word.WriteRune(r) + wordLen++ + } else { + addWord() + buf.WriteRune(r) + curWidth++ + } + default: + word.WriteRune(r) + wordLen++ + + if wordLen == limit { + // Hardwrap the word if it's too long + addWord() + } + + if curWidth+wordLen+space.Len() > limit { + addNewline() + } + } + + default: + word.WriteByte(b[i]) + } + + // We manage the UTF8 state separately manually above. + if pstate != parser.Utf8State { + pstate = state + } + i++ + } + + if word.Len() != 0 { + // Preserve ANSI wrapped spaces at the end of string + if curWidth+space.Len() > limit { + buf.WriteByte('\n') + } + addSpace() + } + buf.Write(word.Bytes()) + + return buf.String() +} + +func runeContainsAny(r rune, s string) bool { + for _, c := range s { + if c == r { + return true + } + } + return false +} diff --git a/vendor/github.com/charmbracelet/x/exp/term/ansi/xterm.go b/vendor/github.com/charmbracelet/x/exp/term/ansi/xterm.go new file mode 100644 index 000000000..e2eb10a15 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/term/ansi/xterm.go @@ -0,0 +1,33 @@ +package ansi + +// DisableModifyOtherKeys disables the modifyOtherKeys mode. +// +// CSI > 4 ; 0 m +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ +// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys +const DisableModifyOtherKeys = "\x1b[>4;0m" + +// EnableModifyOtherKeys1 enables the modifyOtherKeys mode 1. +// +// CSI > 4 ; 1 m +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ +// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys +const EnableModifyOtherKeys1 = "\x1b[>4;1m" + +// EnableModifyOtherKeys2 enables the modifyOtherKeys mode 2. +// +// CSI > 4 ; 2 m +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ +// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys +const EnableModifyOtherKeys2 = "\x1b[>4;2m" + +// RequestModifyOtherKeys requests the modifyOtherKeys mode. +// +// CSI ? 4 m +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ +// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys +const RequestModifyOtherKeys = "\x1b[?4m" diff --git a/vendor/github.com/cli/browser/LICENSE b/vendor/github.com/cli/browser/LICENSE new file mode 100644 index 000000000..65f78fb62 --- /dev/null +++ b/vendor/github.com/cli/browser/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2014, Dave Cheney +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/cli/browser/README.md b/vendor/github.com/cli/browser/README.md new file mode 100644 index 000000000..8a463155f --- /dev/null +++ b/vendor/github.com/cli/browser/README.md @@ -0,0 +1,20 @@ + +# browser + +Helpers to open URLs, readers, or files in the system default web browser. + +This fork adds: + +- `OpenReader` error wrapping; +- `ErrNotFound` error wrapping on BSD; +- Go 1.21 support. + +## Usage + +``` go +import "github.com/cli/browser" + +err = browser.OpenURL(url) +err = browser.OpenFile(path) +err = browser.OpenReader(reader) +``` diff --git a/vendor/github.com/cli/browser/browser.go b/vendor/github.com/cli/browser/browser.go new file mode 100644 index 000000000..a0bd87ea9 --- /dev/null +++ b/vendor/github.com/cli/browser/browser.go @@ -0,0 +1,57 @@ +// Package browser provides helpers to open files, readers, and urls in a browser window. +// +// The choice of which browser is started is entirely client dependent. +package browser + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" +) + +// Stdout is the io.Writer to which executed commands write standard output. +var Stdout io.Writer = os.Stdout + +// Stderr is the io.Writer to which executed commands write standard error. +var Stderr io.Writer = os.Stderr + +// OpenFile opens new browser window for the file path. +func OpenFile(path string) error { + path, err := filepath.Abs(path) + if err != nil { + return err + } + return OpenURL("file://" + path) +} + +// OpenReader consumes the contents of r and presents the +// results in a new browser window. +func OpenReader(r io.Reader) error { + f, err := ioutil.TempFile("", "browser.*.html") + if err != nil { + return fmt.Errorf("browser: could not create temporary file: %w", err) + } + if _, err := io.Copy(f, r); err != nil { + f.Close() + return fmt.Errorf("browser: caching temporary file failed: %w", err) + } + if err := f.Close(); err != nil { + return fmt.Errorf("browser: caching temporary file failed: %w", err) + } + return OpenFile(f.Name()) +} + +// OpenURL opens a new browser window pointing to url. +func OpenURL(url string) error { + return openBrowser(url) +} + +func runCmd(prog string, args ...string) error { + cmd := exec.Command(prog, args...) + cmd.Stdout = Stdout + cmd.Stderr = Stderr + return cmd.Run() +} diff --git a/vendor/github.com/cli/browser/browser_darwin.go b/vendor/github.com/cli/browser/browser_darwin.go new file mode 100644 index 000000000..8507cf7c2 --- /dev/null +++ b/vendor/github.com/cli/browser/browser_darwin.go @@ -0,0 +1,5 @@ +package browser + +func openBrowser(url string) error { + return runCmd("open", url) +} diff --git a/vendor/github.com/cli/browser/browser_freebsd.go b/vendor/github.com/cli/browser/browser_freebsd.go new file mode 100644 index 000000000..2a3c9a2e8 --- /dev/null +++ b/vendor/github.com/cli/browser/browser_freebsd.go @@ -0,0 +1,15 @@ +package browser + +import ( + "errors" + "fmt" + "os/exec" +) + +func openBrowser(url string) error { + err := runCmd("xdg-open", url) + if errors.Is(err, exec.ErrNotFound) { + return fmt.Errorf("%w - install xdg-utils from ports(8)", err) + } + return err +} diff --git a/vendor/github.com/cli/browser/browser_linux.go b/vendor/github.com/cli/browser/browser_linux.go new file mode 100644 index 000000000..b30325015 --- /dev/null +++ b/vendor/github.com/cli/browser/browser_linux.go @@ -0,0 +1,21 @@ +package browser + +import ( + "os/exec" + "strings" +) + +func openBrowser(url string) error { + providers := []string{"xdg-open", "x-www-browser", "www-browser", "wslview"} + + // There are multiple possible providers to open a browser on linux + // One of them is xdg-open, another is x-www-browser, then there's www-browser, etc. + // Look for one that exists and run it + for _, provider := range providers { + if _, err := exec.LookPath(provider); err == nil { + return runCmd(provider, url) + } + } + + return &exec.Error{Name: strings.Join(providers, ","), Err: exec.ErrNotFound} +} diff --git a/vendor/github.com/cli/browser/browser_netbsd.go b/vendor/github.com/cli/browser/browser_netbsd.go new file mode 100644 index 000000000..65a5e5a29 --- /dev/null +++ b/vendor/github.com/cli/browser/browser_netbsd.go @@ -0,0 +1,14 @@ +package browser + +import ( + "errors" + "os/exec" +) + +func openBrowser(url string) error { + err := runCmd("xdg-open", url) + if e, ok := err.(*exec.Error); ok && e.Err == exec.ErrNotFound { + return errors.New("xdg-open: command not found - install xdg-utils from pkgsrc(7)") + } + return err +} diff --git a/vendor/github.com/cli/browser/browser_openbsd.go b/vendor/github.com/cli/browser/browser_openbsd.go new file mode 100644 index 000000000..2a3c9a2e8 --- /dev/null +++ b/vendor/github.com/cli/browser/browser_openbsd.go @@ -0,0 +1,15 @@ +package browser + +import ( + "errors" + "fmt" + "os/exec" +) + +func openBrowser(url string) error { + err := runCmd("xdg-open", url) + if errors.Is(err, exec.ErrNotFound) { + return fmt.Errorf("%w - install xdg-utils from ports(8)", err) + } + return err +} diff --git a/vendor/github.com/cli/browser/browser_unsupported.go b/vendor/github.com/cli/browser/browser_unsupported.go new file mode 100644 index 000000000..7c5c17d34 --- /dev/null +++ b/vendor/github.com/cli/browser/browser_unsupported.go @@ -0,0 +1,12 @@ +// +build !linux,!windows,!darwin,!openbsd,!freebsd,!netbsd + +package browser + +import ( + "fmt" + "runtime" +) + +func openBrowser(url string) error { + return fmt.Errorf("openBrowser: unsupported operating system: %v", runtime.GOOS) +} diff --git a/vendor/github.com/cli/browser/browser_windows.go b/vendor/github.com/cli/browser/browser_windows.go new file mode 100644 index 000000000..63e192959 --- /dev/null +++ b/vendor/github.com/cli/browser/browser_windows.go @@ -0,0 +1,7 @@ +package browser + +import "golang.org/x/sys/windows" + +func openBrowser(url string) error { + return windows.ShellExecute(0, nil, windows.StringToUTF16Ptr(url), nil, nil, windows.SW_SHOWNORMAL) +} diff --git a/vendor/github.com/cli/cli/v2/LICENSE b/vendor/github.com/cli/cli/v2/LICENSE new file mode 100644 index 000000000..b6a58a957 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 GitHub Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/cli/cli/v2/api/client.go b/vendor/github.com/cli/cli/v2/api/client.go new file mode 100644 index 000000000..e32856554 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/api/client.go @@ -0,0 +1,273 @@ +package api + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "regexp" + "strings" + + "github.com/cli/cli/v2/internal/ghinstance" + ghAPI "github.com/cli/go-gh/v2/pkg/api" +) + +const ( + accept = "Accept" + authorization = "Authorization" + cacheTTL = "X-GH-CACHE-TTL" + graphqlFeatures = "GraphQL-Features" + features = "merge_queue" + userAgent = "User-Agent" +) + +var linkRE = regexp.MustCompile(`<([^>]+)>;\s*rel="([^"]+)"`) + +func NewClientFromHTTP(httpClient *http.Client) *Client { + client := &Client{http: httpClient} + return client +} + +type Client struct { + http *http.Client +} + +func (c *Client) HTTP() *http.Client { + return c.http +} + +type GraphQLError struct { + *ghAPI.GraphQLError +} + +type HTTPError struct { + *ghAPI.HTTPError + scopesSuggestion string +} + +func (err HTTPError) ScopesSuggestion() string { + return err.scopesSuggestion +} + +// GraphQL performs a GraphQL request using the query string and parses the response into data receiver. If there are errors in the response, +// GraphQLError will be returned, but the receiver will also be partially populated. +func (c Client) GraphQL(hostname string, query string, variables map[string]interface{}, data interface{}) error { + opts := clientOptions(hostname, c.http.Transport) + opts.Headers[graphqlFeatures] = features + gqlClient, err := ghAPI.NewGraphQLClient(opts) + if err != nil { + return err + } + return handleResponse(gqlClient.Do(query, variables, data)) +} + +// Mutate performs a GraphQL mutation based on a struct and parses the response with the same struct as the receiver. If there are errors in the response, +// GraphQLError will be returned, but the receiver will also be partially populated. +func (c Client) Mutate(hostname, name string, mutation interface{}, variables map[string]interface{}) error { + opts := clientOptions(hostname, c.http.Transport) + opts.Headers[graphqlFeatures] = features + gqlClient, err := ghAPI.NewGraphQLClient(opts) + if err != nil { + return err + } + return handleResponse(gqlClient.Mutate(name, mutation, variables)) +} + +// Query performs a GraphQL query based on a struct and parses the response with the same struct as the receiver. If there are errors in the response, +// GraphQLError will be returned, but the receiver will also be partially populated. +func (c Client) Query(hostname, name string, query interface{}, variables map[string]interface{}) error { + opts := clientOptions(hostname, c.http.Transport) + opts.Headers[graphqlFeatures] = features + gqlClient, err := ghAPI.NewGraphQLClient(opts) + if err != nil { + return err + } + return handleResponse(gqlClient.Query(name, query, variables)) +} + +// QueryWithContext performs a GraphQL query based on a struct and parses the response with the same struct as the receiver. If there are errors in the response, +// GraphQLError will be returned, but the receiver will also be partially populated. +func (c Client) QueryWithContext(ctx context.Context, hostname, name string, query interface{}, variables map[string]interface{}) error { + opts := clientOptions(hostname, c.http.Transport) + opts.Headers[graphqlFeatures] = features + gqlClient, err := ghAPI.NewGraphQLClient(opts) + if err != nil { + return err + } + return handleResponse(gqlClient.QueryWithContext(ctx, name, query, variables)) +} + +// REST performs a REST request and parses the response. +func (c Client) REST(hostname string, method string, p string, body io.Reader, data interface{}) error { + opts := clientOptions(hostname, c.http.Transport) + restClient, err := ghAPI.NewRESTClient(opts) + if err != nil { + return err + } + return handleResponse(restClient.Do(method, p, body, data)) +} + +func (c Client) RESTWithNext(hostname string, method string, p string, body io.Reader, data interface{}) (string, error) { + opts := clientOptions(hostname, c.http.Transport) + restClient, err := ghAPI.NewRESTClient(opts) + if err != nil { + return "", err + } + + resp, err := restClient.Request(method, p, body) + if err != nil { + return "", err + } + defer resp.Body.Close() + + success := resp.StatusCode >= 200 && resp.StatusCode < 300 + if !success { + return "", HandleHTTPError(resp) + } + + if resp.StatusCode == http.StatusNoContent { + return "", nil + } + + b, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + err = json.Unmarshal(b, &data) + if err != nil { + return "", err + } + + var next string + for _, m := range linkRE.FindAllStringSubmatch(resp.Header.Get("Link"), -1) { + if len(m) > 2 && m[2] == "next" { + next = m[1] + } + } + + return next, nil +} + +// HandleHTTPError parses a http.Response into a HTTPError. +func HandleHTTPError(resp *http.Response) error { + return handleResponse(ghAPI.HandleHTTPError(resp)) +} + +// handleResponse takes a ghAPI.HTTPError or ghAPI.GraphQLError and converts it into an +// HTTPError or GraphQLError respectively. +func handleResponse(err error) error { + if err == nil { + return nil + } + + var restErr *ghAPI.HTTPError + if errors.As(err, &restErr) { + return HTTPError{ + HTTPError: restErr, + scopesSuggestion: generateScopesSuggestion(restErr.StatusCode, + restErr.Headers.Get("X-Accepted-Oauth-Scopes"), + restErr.Headers.Get("X-Oauth-Scopes"), + restErr.RequestURL.Hostname()), + } + } + + var gqlErr *ghAPI.GraphQLError + if errors.As(err, &gqlErr) { + return GraphQLError{ + GraphQLError: gqlErr, + } + } + + return err +} + +// ScopesSuggestion is an error messaging utility that prints the suggestion to request additional OAuth +// scopes in case a server response indicates that there are missing scopes. +func ScopesSuggestion(resp *http.Response) string { + return generateScopesSuggestion(resp.StatusCode, + resp.Header.Get("X-Accepted-Oauth-Scopes"), + resp.Header.Get("X-Oauth-Scopes"), + resp.Request.URL.Hostname()) +} + +// EndpointNeedsScopes adds additional OAuth scopes to an HTTP response as if they were returned from the +// server endpoint. This improves HTTP 4xx error messaging for endpoints that don't explicitly list the +// OAuth scopes they need. +func EndpointNeedsScopes(resp *http.Response, s string) *http.Response { + if resp.StatusCode >= 400 && resp.StatusCode < 500 { + oldScopes := resp.Header.Get("X-Accepted-Oauth-Scopes") + resp.Header.Set("X-Accepted-Oauth-Scopes", fmt.Sprintf("%s, %s", oldScopes, s)) + } + return resp +} + +func generateScopesSuggestion(statusCode int, endpointNeedsScopes, tokenHasScopes, hostname string) string { + if statusCode < 400 || statusCode > 499 || statusCode == 422 { + return "" + } + + if tokenHasScopes == "" { + return "" + } + + gotScopes := map[string]struct{}{} + for _, s := range strings.Split(tokenHasScopes, ",") { + s = strings.TrimSpace(s) + gotScopes[s] = struct{}{} + + // Certain scopes may be grouped under a single "top-level" scope. The following branch + // statements include these grouped/implied scopes when the top-level scope is encountered. + // See https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps. + if s == "repo" { + gotScopes["repo:status"] = struct{}{} + gotScopes["repo_deployment"] = struct{}{} + gotScopes["public_repo"] = struct{}{} + gotScopes["repo:invite"] = struct{}{} + gotScopes["security_events"] = struct{}{} + } else if s == "user" { + gotScopes["read:user"] = struct{}{} + gotScopes["user:email"] = struct{}{} + gotScopes["user:follow"] = struct{}{} + } else if s == "codespace" { + gotScopes["codespace:secrets"] = struct{}{} + } else if strings.HasPrefix(s, "admin:") { + gotScopes["read:"+strings.TrimPrefix(s, "admin:")] = struct{}{} + gotScopes["write:"+strings.TrimPrefix(s, "admin:")] = struct{}{} + } else if strings.HasPrefix(s, "write:") { + gotScopes["read:"+strings.TrimPrefix(s, "write:")] = struct{}{} + } + } + + for _, s := range strings.Split(endpointNeedsScopes, ",") { + s = strings.TrimSpace(s) + if _, gotScope := gotScopes[s]; s == "" || gotScope { + continue + } + return fmt.Sprintf( + "This API operation needs the %[1]q scope. To request it, run: gh auth refresh -h %[2]s -s %[1]s", + s, + ghinstance.NormalizeHostname(hostname), + ) + } + + return "" +} + +func clientOptions(hostname string, transport http.RoundTripper) ghAPI.ClientOptions { + // AuthToken, and Headers are being handled by transport, + // so let go-gh know that it does not need to resolve them. + opts := ghAPI.ClientOptions{ + AuthToken: "none", + Headers: map[string]string{ + authorization: "", + }, + Host: hostname, + SkipDefaultHeaders: true, + Transport: transport, + LogIgnoreEnv: true, + } + return opts +} diff --git a/vendor/github.com/cli/cli/v2/api/export_pr.go b/vendor/github.com/cli/cli/v2/api/export_pr.go new file mode 100644 index 000000000..bb3310811 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/api/export_pr.go @@ -0,0 +1,155 @@ +package api + +import ( + "reflect" + "strings" +) + +func (issue *Issue) ExportData(fields []string) map[string]interface{} { + v := reflect.ValueOf(issue).Elem() + data := map[string]interface{}{} + + for _, f := range fields { + switch f { + case "comments": + data[f] = issue.Comments.Nodes + case "assignees": + data[f] = issue.Assignees.Nodes + case "labels": + data[f] = issue.Labels.Nodes + case "projectCards": + data[f] = issue.ProjectCards.Nodes + case "projectItems": + items := make([]map[string]interface{}, 0, len(issue.ProjectItems.Nodes)) + for _, n := range issue.ProjectItems.Nodes { + items = append(items, map[string]interface{}{ + "status": n.Status, + "title": n.Project.Title, + }) + } + data[f] = items + default: + sf := fieldByName(v, f) + data[f] = sf.Interface() + } + } + + return data +} + +func (pr *PullRequest) ExportData(fields []string) map[string]interface{} { + v := reflect.ValueOf(pr).Elem() + data := map[string]interface{}{} + + for _, f := range fields { + switch f { + case "headRepository": + data[f] = pr.HeadRepository + case "statusCheckRollup": + if n := pr.StatusCheckRollup.Nodes; len(n) > 0 { + checks := make([]interface{}, 0, len(n[0].Commit.StatusCheckRollup.Contexts.Nodes)) + for _, c := range n[0].Commit.StatusCheckRollup.Contexts.Nodes { + if c.TypeName == "CheckRun" { + checks = append(checks, map[string]interface{}{ + "__typename": c.TypeName, + "name": c.Name, + "workflowName": c.CheckSuite.WorkflowRun.Workflow.Name, + "status": c.Status, + "conclusion": c.Conclusion, + "startedAt": c.StartedAt, + "completedAt": c.CompletedAt, + "detailsUrl": c.DetailsURL, + }) + } else { + checks = append(checks, map[string]interface{}{ + "__typename": c.TypeName, + "context": c.Context, + "state": c.State, + "targetUrl": c.TargetURL, + "startedAt": c.CreatedAt, + }) + } + } + data[f] = checks + } else { + data[f] = nil + } + case "commits": + commits := make([]interface{}, 0, len(pr.Commits.Nodes)) + for _, c := range pr.Commits.Nodes { + commit := c.Commit + authors := make([]interface{}, 0, len(commit.Authors.Nodes)) + for _, author := range commit.Authors.Nodes { + authors = append(authors, map[string]interface{}{ + "name": author.Name, + "email": author.Email, + "id": author.User.ID, + "login": author.User.Login, + }) + } + commits = append(commits, map[string]interface{}{ + "oid": commit.OID, + "messageHeadline": commit.MessageHeadline, + "messageBody": commit.MessageBody, + "committedDate": commit.CommittedDate, + "authoredDate": commit.AuthoredDate, + "authors": authors, + }) + } + data[f] = commits + case "comments": + data[f] = pr.Comments.Nodes + case "assignees": + data[f] = pr.Assignees.Nodes + case "labels": + data[f] = pr.Labels.Nodes + case "projectCards": + data[f] = pr.ProjectCards.Nodes + case "projectItems": + items := make([]map[string]interface{}, 0, len(pr.ProjectItems.Nodes)) + for _, n := range pr.ProjectItems.Nodes { + items = append(items, map[string]interface{}{ + "status": n.Status, + "title": n.Project.Title, + }) + } + data[f] = items + case "reviews": + data[f] = pr.Reviews.Nodes + case "latestReviews": + data[f] = pr.LatestReviews.Nodes + case "files": + data[f] = pr.Files.Nodes + case "reviewRequests": + requests := make([]interface{}, 0, len(pr.ReviewRequests.Nodes)) + for _, req := range pr.ReviewRequests.Nodes { + r := req.RequestedReviewer + switch r.TypeName { + case "User": + requests = append(requests, map[string]string{ + "__typename": r.TypeName, + "login": r.Login, + }) + case "Team": + requests = append(requests, map[string]string{ + "__typename": r.TypeName, + "name": r.Name, + "slug": r.LoginOrSlug(), + }) + } + } + data[f] = &requests + default: + sf := fieldByName(v, f) + data[f] = sf.Interface() + } + } + + return data +} + +func fieldByName(v reflect.Value, field string) reflect.Value { + return v.FieldByNameFunc(func(s string) bool { + return strings.EqualFold(field, s) + }) +} diff --git a/vendor/github.com/cli/cli/v2/api/export_repo.go b/vendor/github.com/cli/cli/v2/api/export_repo.go new file mode 100644 index 000000000..a07246ab9 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/api/export_repo.go @@ -0,0 +1,53 @@ +package api + +import ( + "reflect" +) + +func (repo *Repository) ExportData(fields []string) map[string]interface{} { + v := reflect.ValueOf(repo).Elem() + data := map[string]interface{}{} + + for _, f := range fields { + switch f { + case "parent": + data[f] = miniRepoExport(repo.Parent) + case "templateRepository": + data[f] = miniRepoExport(repo.TemplateRepository) + case "languages": + data[f] = repo.Languages.Edges + case "labels": + data[f] = repo.Labels.Nodes + case "assignableUsers": + data[f] = repo.AssignableUsers.Nodes + case "mentionableUsers": + data[f] = repo.MentionableUsers.Nodes + case "milestones": + data[f] = repo.Milestones.Nodes + case "projects": + data[f] = repo.Projects.Nodes + case "repositoryTopics": + var topics []RepositoryTopic + for _, n := range repo.RepositoryTopics.Nodes { + topics = append(topics, n.Topic) + } + data[f] = topics + default: + sf := fieldByName(v, f) + data[f] = sf.Interface() + } + } + + return data +} + +func miniRepoExport(r *Repository) map[string]interface{} { + if r == nil { + return nil + } + return map[string]interface{}{ + "id": r.ID, + "name": r.Name, + "owner": r.Owner, + } +} diff --git a/vendor/github.com/cli/cli/v2/api/http_client.go b/vendor/github.com/cli/cli/v2/api/http_client.go new file mode 100644 index 000000000..f6e133f1f --- /dev/null +++ b/vendor/github.com/cli/cli/v2/api/http_client.go @@ -0,0 +1,140 @@ +package api + +import ( + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/cli/cli/v2/internal/ghinstance" + "github.com/cli/cli/v2/utils" + ghAPI "github.com/cli/go-gh/v2/pkg/api" +) + +type tokenGetter interface { + ActiveToken(string) (string, string) +} + +type HTTPClientOptions struct { + AppVersion string + CacheTTL time.Duration + Config tokenGetter + EnableCache bool + Log io.Writer + LogColorize bool + LogVerboseHTTP bool +} + +func NewHTTPClient(opts HTTPClientOptions) (*http.Client, error) { + // Provide invalid host, and token values so gh.HTTPClient will not automatically resolve them. + // The real host and token are inserted at request time. + clientOpts := ghAPI.ClientOptions{ + Host: "none", + AuthToken: "none", + LogIgnoreEnv: true, + } + + debugEnabled, debugValue := utils.IsDebugEnabled() + if strings.Contains(debugValue, "api") { + opts.LogVerboseHTTP = true + } + + if opts.LogVerboseHTTP || debugEnabled { + clientOpts.Log = opts.Log + clientOpts.LogColorize = opts.LogColorize + clientOpts.LogVerboseHTTP = opts.LogVerboseHTTP + } + + headers := map[string]string{ + userAgent: fmt.Sprintf("GitHub CLI %s", opts.AppVersion), + } + clientOpts.Headers = headers + + if opts.EnableCache { + clientOpts.EnableCache = opts.EnableCache + clientOpts.CacheTTL = opts.CacheTTL + } + + client, err := ghAPI.NewHTTPClient(clientOpts) + if err != nil { + return nil, err + } + + if opts.Config != nil { + client.Transport = AddAuthTokenHeader(client.Transport, opts.Config) + } + + return client, nil +} + +func NewCachedHTTPClient(httpClient *http.Client, ttl time.Duration) *http.Client { + newClient := *httpClient + newClient.Transport = AddCacheTTLHeader(httpClient.Transport, ttl) + return &newClient +} + +// AddCacheTTLHeader adds an header to the request telling the cache that the request +// should be cached for a specified amount of time. +func AddCacheTTLHeader(rt http.RoundTripper, ttl time.Duration) http.RoundTripper { + return &funcTripper{roundTrip: func(req *http.Request) (*http.Response, error) { + // If the header is already set in the request, don't overwrite it. + if req.Header.Get(cacheTTL) == "" { + req.Header.Set(cacheTTL, ttl.String()) + } + return rt.RoundTrip(req) + }} +} + +// AddAuthToken adds an authentication token header for the host specified by the request. +func AddAuthTokenHeader(rt http.RoundTripper, cfg tokenGetter) http.RoundTripper { + return &funcTripper{roundTrip: func(req *http.Request) (*http.Response, error) { + // If the header is already set in the request, don't overwrite it. + if req.Header.Get(authorization) == "" { + var redirectHostnameChange bool + if req.Response != nil && req.Response.Request != nil { + redirectHostnameChange = getHost(req) != getHost(req.Response.Request) + } + // Only set header if an initial request or redirect request to the same host as the initial request. + // If the host has changed during a redirect do not add the authentication token header. + if !redirectHostnameChange { + hostname := ghinstance.NormalizeHostname(getHost(req)) + if token, _ := cfg.ActiveToken(hostname); token != "" { + req.Header.Set(authorization, fmt.Sprintf("token %s", token)) + } + } + } + return rt.RoundTrip(req) + }} +} + +// ExtractHeader extracts a named header from any response received by this client and, +// if non-blank, saves it to dest. +func ExtractHeader(name string, dest *string) func(http.RoundTripper) http.RoundTripper { + return func(tr http.RoundTripper) http.RoundTripper { + return &funcTripper{roundTrip: func(req *http.Request) (*http.Response, error) { + res, err := tr.RoundTrip(req) + if err == nil { + if value := res.Header.Get(name); value != "" { + *dest = value + } + } + return res, err + }} + } +} + +type funcTripper struct { + roundTrip func(*http.Request) (*http.Response, error) +} + +func (tr funcTripper) RoundTrip(req *http.Request) (*http.Response, error) { + return tr.roundTrip(req) +} + +func getHost(r *http.Request) string { + if r.Host != "" { + return r.Host + } + return r.URL.Host +} diff --git a/vendor/github.com/cli/cli/v2/api/queries_branch_issue_reference.go b/vendor/github.com/cli/cli/v2/api/queries_branch_issue_reference.go new file mode 100644 index 000000000..16e36831b --- /dev/null +++ b/vendor/github.com/cli/cli/v2/api/queries_branch_issue_reference.go @@ -0,0 +1,145 @@ +package api + +import ( + "fmt" + + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/shurcooL/githubv4" +) + +type LinkedBranch struct { + BranchName string + URL string +} + +func CreateLinkedBranch(client *Client, host string, repoID, issueID, branchID, branchName string) (string, error) { + var mutation struct { + CreateLinkedBranch struct { + LinkedBranch struct { + ID string + Ref struct { + Name string + } + } + } `graphql:"createLinkedBranch(input: $input)"` + } + + input := githubv4.CreateLinkedBranchInput{ + IssueID: githubv4.ID(issueID), + Oid: githubv4.GitObjectID(branchID), + } + if repoID != "" { + repo := githubv4.ID(repoID) + input.RepositoryID = &repo + } + if branchName != "" { + name := githubv4.String(branchName) + input.Name = &name + } + variables := map[string]interface{}{ + "input": input, + } + + err := client.Mutate(host, "CreateLinkedBranch", &mutation, variables) + if err != nil { + return "", err + } + + return mutation.CreateLinkedBranch.LinkedBranch.Ref.Name, nil +} + +func ListLinkedBranches(client *Client, repo ghrepo.Interface, issueNumber int) ([]LinkedBranch, error) { + var query struct { + Repository struct { + Issue struct { + LinkedBranches struct { + Nodes []struct { + Ref struct { + Name string + Repository struct { + Url string + } + } + } + } `graphql:"linkedBranches(first: 30)"` + } `graphql:"issue(number: $number)"` + } `graphql:"repository(owner: $owner, name: $name)"` + } + + variables := map[string]interface{}{ + "number": githubv4.Int(issueNumber), + "owner": githubv4.String(repo.RepoOwner()), + "name": githubv4.String(repo.RepoName()), + } + + if err := client.Query(repo.RepoHost(), "ListLinkedBranches", &query, variables); err != nil { + return []LinkedBranch{}, err + } + + var branchNames []LinkedBranch + + for _, node := range query.Repository.Issue.LinkedBranches.Nodes { + branch := LinkedBranch{ + BranchName: node.Ref.Name, + URL: fmt.Sprintf("%s/tree/%s", node.Ref.Repository.Url, node.Ref.Name), + } + branchNames = append(branchNames, branch) + } + + return branchNames, nil +} + +func CheckLinkedBranchFeature(client *Client, host string) error { + var query struct { + Name struct { + Fields []struct { + Name string + } + } `graphql:"LinkedBranch: __type(name: \"LinkedBranch\")"` + } + + if err := client.Query(host, "LinkedBranchFeature", &query, nil); err != nil { + return err + } + + if len(query.Name.Fields) == 0 { + return fmt.Errorf("the `gh issue develop` command is not currently available") + } + + return nil +} + +func FindRepoBranchID(client *Client, repo ghrepo.Interface, ref string) (string, string, error) { + var query struct { + Repository struct { + Id string + DefaultBranchRef struct { + Target struct { + Oid string + } + } + Ref struct { + Target struct { + Oid string + } + } `graphql:"ref(qualifiedName: $ref)"` + } `graphql:"repository(owner: $owner, name: $name)"` + } + + variables := map[string]interface{}{ + "ref": githubv4.String(ref), + "owner": githubv4.String(repo.RepoOwner()), + "name": githubv4.String(repo.RepoName()), + } + + if err := client.Query(repo.RepoHost(), "FindRepoBranchID", &query, variables); err != nil { + return "", "", err + } + + branchID := query.Repository.Ref.Target.Oid + if branchID == "" { + branchID = query.Repository.DefaultBranchRef.Target.Oid + } + + return query.Repository.Id, branchID, nil +} diff --git a/vendor/github.com/cli/cli/v2/api/queries_comments.go b/vendor/github.com/cli/cli/v2/api/queries_comments.go new file mode 100644 index 000000000..5cc84a3e4 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/api/queries_comments.go @@ -0,0 +1,144 @@ +package api + +import ( + "time" + + "github.com/shurcooL/githubv4" +) + +type Comments struct { + Nodes []Comment + TotalCount int + PageInfo struct { + HasNextPage bool + EndCursor string + } +} + +func (cs Comments) CurrentUserComments() []Comment { + var comments []Comment + for _, c := range cs.Nodes { + if c.ViewerDidAuthor { + comments = append(comments, c) + } + } + return comments +} + +type Comment struct { + ID string `json:"id"` + Author CommentAuthor `json:"author"` + AuthorAssociation string `json:"authorAssociation"` + Body string `json:"body"` + CreatedAt time.Time `json:"createdAt"` + IncludesCreatedEdit bool `json:"includesCreatedEdit"` + IsMinimized bool `json:"isMinimized"` + MinimizedReason string `json:"minimizedReason"` + ReactionGroups ReactionGroups `json:"reactionGroups"` + URL string `json:"url,omitempty"` + ViewerDidAuthor bool `json:"viewerDidAuthor"` +} + +type CommentCreateInput struct { + Body string + SubjectId string +} + +type CommentUpdateInput struct { + Body string + CommentId string +} + +func CommentCreate(client *Client, repoHost string, params CommentCreateInput) (string, error) { + var mutation struct { + AddComment struct { + CommentEdge struct { + Node struct { + URL string + } + } + } `graphql:"addComment(input: $input)"` + } + + variables := map[string]interface{}{ + "input": githubv4.AddCommentInput{ + Body: githubv4.String(params.Body), + SubjectID: githubv4.ID(params.SubjectId), + }, + } + + err := client.Mutate(repoHost, "CommentCreate", &mutation, variables) + if err != nil { + return "", err + } + + return mutation.AddComment.CommentEdge.Node.URL, nil +} + +func CommentUpdate(client *Client, repoHost string, params CommentUpdateInput) (string, error) { + var mutation struct { + UpdateIssueComment struct { + IssueComment struct { + URL string + } + } `graphql:"updateIssueComment(input: $input)"` + } + + variables := map[string]interface{}{ + "input": githubv4.UpdateIssueCommentInput{ + Body: githubv4.String(params.Body), + ID: githubv4.ID(params.CommentId), + }, + } + + err := client.Mutate(repoHost, "CommentUpdate", &mutation, variables) + if err != nil { + return "", err + } + + return mutation.UpdateIssueComment.IssueComment.URL, nil +} + +func (c Comment) Identifier() string { + return c.ID +} + +func (c Comment) AuthorLogin() string { + return c.Author.Login +} + +func (c Comment) Association() string { + return c.AuthorAssociation +} + +func (c Comment) Content() string { + return c.Body +} + +func (c Comment) Created() time.Time { + return c.CreatedAt +} + +func (c Comment) HiddenReason() string { + return c.MinimizedReason +} + +func (c Comment) IsEdited() bool { + return c.IncludesCreatedEdit +} + +func (c Comment) IsHidden() bool { + return c.IsMinimized +} + +func (c Comment) Link() string { + return c.URL +} + +func (c Comment) Reactions() ReactionGroups { + return c.ReactionGroups +} + +func (c Comment) Status() string { + return "" +} diff --git a/vendor/github.com/cli/cli/v2/api/queries_issue.go b/vendor/github.com/cli/cli/v2/api/queries_issue.go new file mode 100644 index 000000000..62531e84e --- /dev/null +++ b/vendor/github.com/cli/cli/v2/api/queries_issue.go @@ -0,0 +1,335 @@ +package api + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/cli/cli/v2/internal/ghrepo" +) + +type IssuesPayload struct { + Assigned IssuesAndTotalCount + Mentioned IssuesAndTotalCount + Authored IssuesAndTotalCount +} + +type IssuesAndTotalCount struct { + Issues []Issue + TotalCount int + SearchCapped bool +} + +type Issue struct { + Typename string `json:"__typename"` + ID string + Number int + Title string + URL string + State string + StateReason string + Closed bool + Body string + ActiveLockReason string + Locked bool + CreatedAt time.Time + UpdatedAt time.Time + ClosedAt *time.Time + Comments Comments + Author Author + Assignees Assignees + Labels Labels + ProjectCards ProjectCards + ProjectItems ProjectItems + Milestone *Milestone + ReactionGroups ReactionGroups + IsPinned bool +} + +// return values for Issue.Typename +const ( + TypeIssue string = "Issue" + TypePullRequest string = "PullRequest" +) + +func (i Issue) IsPullRequest() bool { + return i.Typename == TypePullRequest +} + +type Assignees struct { + Nodes []GitHubUser + TotalCount int +} + +func (a Assignees) Logins() []string { + logins := make([]string, len(a.Nodes)) + for i, a := range a.Nodes { + logins[i] = a.Login + } + return logins +} + +type Labels struct { + Nodes []IssueLabel + TotalCount int +} + +func (l Labels) Names() []string { + names := make([]string, len(l.Nodes)) + for i, l := range l.Nodes { + names[i] = l.Name + } + return names +} + +type ProjectCards struct { + Nodes []*ProjectInfo + TotalCount int +} + +type ProjectItems struct { + Nodes []*ProjectV2Item +} + +type ProjectInfo struct { + Project struct { + Name string `json:"name"` + } `json:"project"` + Column struct { + Name string `json:"name"` + } `json:"column"` +} + +type ProjectV2Item struct { + ID string `json:"id"` + Project ProjectV2ItemProject + Status ProjectV2ItemStatus +} + +type ProjectV2ItemProject struct { + ID string `json:"id"` + Title string `json:"title"` +} + +type ProjectV2ItemStatus struct { + OptionID string `json:"optionId"` + Name string `json:"name"` +} + +func (p ProjectCards) ProjectNames() []string { + names := make([]string, len(p.Nodes)) + for i, c := range p.Nodes { + names[i] = c.Project.Name + } + return names +} + +func (p ProjectItems) ProjectTitles() []string { + titles := make([]string, len(p.Nodes)) + for i, c := range p.Nodes { + titles[i] = c.Project.Title + } + return titles +} + +type Milestone struct { + Number int `json:"number"` + Title string `json:"title"` + Description string `json:"description"` + DueOn *time.Time `json:"dueOn"` +} + +type IssuesDisabledError struct { + error +} + +type Owner struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Login string `json:"login"` +} + +type Author struct { + ID string + Name string + Login string +} + +func (author Author) MarshalJSON() ([]byte, error) { + if author.ID == "" { + return json.Marshal(map[string]interface{}{ + "is_bot": true, + "login": "app/" + author.Login, + }) + } + return json.Marshal(map[string]interface{}{ + "is_bot": false, + "login": author.Login, + "id": author.ID, + "name": author.Name, + }) +} + +type CommentAuthor struct { + Login string `json:"login"` + // Unfortunately, there is no easy way to add "id" and "name" fields to this struct because it's being + // used in both shurcool-graphql type queries and string-based queries where the response gets parsed + // by an ordinary JSON decoder that doesn't understand "graphql" directives via struct tags. + // User *struct { + // ID string + // Name string + // } `graphql:"... on User"` +} + +// IssueCreate creates an issue in a GitHub repository +func IssueCreate(client *Client, repo *Repository, params map[string]interface{}) (*Issue, error) { + query := ` + mutation IssueCreate($input: CreateIssueInput!) { + createIssue(input: $input) { + issue { + id + url + } + } + }` + + inputParams := map[string]interface{}{ + "repositoryId": repo.ID, + } + for key, val := range params { + switch key { + case "assigneeIds", "body", "issueTemplate", "labelIds", "milestoneId", "projectIds", "repositoryId", "title": + inputParams[key] = val + case "projectV2Ids": + default: + return nil, fmt.Errorf("invalid IssueCreate mutation parameter %s", key) + } + } + variables := map[string]interface{}{ + "input": inputParams, + } + + result := struct { + CreateIssue struct { + Issue Issue + } + }{} + + err := client.GraphQL(repo.RepoHost(), query, variables, &result) + if err != nil { + return nil, err + } + issue := &result.CreateIssue.Issue + + // projectV2 parameters aren't supported in the `createIssue` mutation, + // so add them after the issue has been created. + projectV2Ids, ok := params["projectV2Ids"].([]string) + if ok { + projectItems := make(map[string]string, len(projectV2Ids)) + for _, p := range projectV2Ids { + projectItems[p] = issue.ID + } + err = UpdateProjectV2Items(client, repo, projectItems, nil) + if err != nil { + return issue, err + } + } + + return issue, nil +} + +type IssueStatusOptions struct { + Username string + Fields []string +} + +func IssueStatus(client *Client, repo ghrepo.Interface, options IssueStatusOptions) (*IssuesPayload, error) { + type response struct { + Repository struct { + Assigned struct { + TotalCount int + Nodes []Issue + } + Mentioned struct { + TotalCount int + Nodes []Issue + } + Authored struct { + TotalCount int + Nodes []Issue + } + HasIssuesEnabled bool + } + } + + fragments := fmt.Sprintf("fragment issue on Issue{%s}", IssueGraphQL(options.Fields)) + query := fragments + ` + query IssueStatus($owner: String!, $repo: String!, $viewer: String!, $per_page: Int = 10) { + repository(owner: $owner, name: $repo) { + hasIssuesEnabled + assigned: issues(filterBy: {assignee: $viewer, states: OPEN}, first: $per_page, orderBy: {field: UPDATED_AT, direction: DESC}) { + totalCount + nodes { + ...issue + } + } + mentioned: issues(filterBy: {mentioned: $viewer, states: OPEN}, first: $per_page, orderBy: {field: UPDATED_AT, direction: DESC}) { + totalCount + nodes { + ...issue + } + } + authored: issues(filterBy: {createdBy: $viewer, states: OPEN}, first: $per_page, orderBy: {field: UPDATED_AT, direction: DESC}) { + totalCount + nodes { + ...issue + } + } + } + }` + + variables := map[string]interface{}{ + "owner": repo.RepoOwner(), + "repo": repo.RepoName(), + "viewer": options.Username, + } + + var resp response + err := client.GraphQL(repo.RepoHost(), query, variables, &resp) + if err != nil { + return nil, err + } + + if !resp.Repository.HasIssuesEnabled { + return nil, fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(repo)) + } + + payload := IssuesPayload{ + Assigned: IssuesAndTotalCount{ + Issues: resp.Repository.Assigned.Nodes, + TotalCount: resp.Repository.Assigned.TotalCount, + }, + Mentioned: IssuesAndTotalCount{ + Issues: resp.Repository.Mentioned.Nodes, + TotalCount: resp.Repository.Mentioned.TotalCount, + }, + Authored: IssuesAndTotalCount{ + Issues: resp.Repository.Authored.Nodes, + TotalCount: resp.Repository.Authored.TotalCount, + }, + } + + return &payload, nil +} + +func (i Issue) Link() string { + return i.URL +} + +func (i Issue) Identifier() string { + return i.ID +} + +func (i Issue) CurrentUserComments() []Comment { + return i.Comments.CurrentUserComments() +} diff --git a/vendor/github.com/cli/cli/v2/api/queries_org.go b/vendor/github.com/cli/cli/v2/api/queries_org.go new file mode 100644 index 000000000..f2e93342e --- /dev/null +++ b/vendor/github.com/cli/cli/v2/api/queries_org.go @@ -0,0 +1,111 @@ +package api + +import ( + "fmt" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/shurcooL/githubv4" +) + +// OrganizationProjects fetches all open projects for an organization. +func OrganizationProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, error) { + type responseData struct { + Organization struct { + Projects struct { + Nodes []RepoProject + PageInfo struct { + HasNextPage bool + EndCursor string + } + } `graphql:"projects(states: [OPEN], first: 100, orderBy: {field: NAME, direction: ASC}, after: $endCursor)"` + } `graphql:"organization(login: $owner)"` + } + + variables := map[string]interface{}{ + "owner": githubv4.String(repo.RepoOwner()), + "endCursor": (*githubv4.String)(nil), + } + + var projects []RepoProject + for { + var query responseData + err := client.Query(repo.RepoHost(), "OrganizationProjectList", &query, variables) + if err != nil { + return nil, err + } + + projects = append(projects, query.Organization.Projects.Nodes...) + if !query.Organization.Projects.PageInfo.HasNextPage { + break + } + variables["endCursor"] = githubv4.String(query.Organization.Projects.PageInfo.EndCursor) + } + + return projects, nil +} + +type OrgTeam struct { + ID string + Slug string +} + +// OrganizationTeam fetch the team in an organization with the given slug +func OrganizationTeam(client *Client, hostname string, org string, teamSlug string) (*OrgTeam, error) { + type responseData struct { + Organization struct { + Team OrgTeam `graphql:"team(slug: $teamSlug)"` + } `graphql:"organization(login: $owner)"` + } + + variables := map[string]interface{}{ + "owner": githubv4.String(org), + "teamSlug": githubv4.String(teamSlug), + } + + var query responseData + err := client.Query(hostname, "OrganizationTeam", &query, variables) + if err != nil { + return nil, err + } + if query.Organization.Team.ID == "" { + return nil, fmt.Errorf("could not resolve to a Team with the slug of '%s'", teamSlug) + } + + return &query.Organization.Team, nil +} + +// OrganizationTeams fetches all the teams in an organization +func OrganizationTeams(client *Client, repo ghrepo.Interface) ([]OrgTeam, error) { + type responseData struct { + Organization struct { + Teams struct { + Nodes []OrgTeam + PageInfo struct { + HasNextPage bool + EndCursor string + } + } `graphql:"teams(first: 100, orderBy: {field: NAME, direction: ASC}, after: $endCursor)"` + } `graphql:"organization(login: $owner)"` + } + + variables := map[string]interface{}{ + "owner": githubv4.String(repo.RepoOwner()), + "endCursor": (*githubv4.String)(nil), + } + + var teams []OrgTeam + for { + var query responseData + err := client.Query(repo.RepoHost(), "OrganizationTeamList", &query, variables) + if err != nil { + return nil, err + } + + teams = append(teams, query.Organization.Teams.Nodes...) + if !query.Organization.Teams.PageInfo.HasNextPage { + break + } + variables["endCursor"] = githubv4.String(query.Organization.Teams.PageInfo.EndCursor) + } + + return teams, nil +} diff --git a/vendor/github.com/cli/cli/v2/api/queries_pr.go b/vendor/github.com/cli/cli/v2/api/queries_pr.go new file mode 100644 index 000000000..36f3c2eef --- /dev/null +++ b/vendor/github.com/cli/cli/v2/api/queries_pr.go @@ -0,0 +1,698 @@ +package api + +import ( + "fmt" + "net/http" + "net/url" + "time" + + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/shurcooL/githubv4" +) + +type PullRequestAndTotalCount struct { + TotalCount int + PullRequests []PullRequest + SearchCapped bool +} + +type PullRequest struct { + ID string + Number int + Title string + State string + Closed bool + URL string + BaseRefName string + HeadRefName string + HeadRefOid string + Body string + Mergeable string + Additions int + Deletions int + ChangedFiles int + MergeStateStatus string + IsInMergeQueue bool + IsMergeQueueEnabled bool // Indicates whether the pull request's base ref has a merge queue enabled. + CreatedAt time.Time + UpdatedAt time.Time + ClosedAt *time.Time + MergedAt *time.Time + + AutoMergeRequest *AutoMergeRequest + + MergeCommit *Commit + PotentialMergeCommit *Commit + + Files struct { + Nodes []PullRequestFile + } + + Author Author + MergedBy *Author + HeadRepositoryOwner Owner + HeadRepository *PRRepository + IsCrossRepository bool + IsDraft bool + MaintainerCanModify bool + + BaseRef struct { + BranchProtectionRule struct { + RequiresStrictStatusChecks bool + RequiredApprovingReviewCount int + } + } + + ReviewDecision string + + Commits struct { + TotalCount int + Nodes []PullRequestCommit + } + StatusCheckRollup struct { + Nodes []StatusCheckRollupNode + } + + Assignees Assignees + Labels Labels + ProjectCards ProjectCards + ProjectItems ProjectItems + Milestone *Milestone + Comments Comments + ReactionGroups ReactionGroups + Reviews PullRequestReviews + LatestReviews PullRequestReviews + ReviewRequests ReviewRequests +} + +type StatusCheckRollupNode struct { + Commit StatusCheckRollupCommit +} + +type StatusCheckRollupCommit struct { + StatusCheckRollup CommitStatusCheckRollup +} + +type CommitStatusCheckRollup struct { + Contexts CheckContexts +} + +// https://docs.github.com/en/graphql/reference/enums#checkrunstate +type CheckRunState string + +const ( + CheckRunStateActionRequired CheckRunState = "ACTION_REQUIRED" + CheckRunStateCancelled CheckRunState = "CANCELLED" + CheckRunStateCompleted CheckRunState = "COMPLETED" + CheckRunStateFailure CheckRunState = "FAILURE" + CheckRunStateInProgress CheckRunState = "IN_PROGRESS" + CheckRunStateNeutral CheckRunState = "NEUTRAL" + CheckRunStatePending CheckRunState = "PENDING" + CheckRunStateQueued CheckRunState = "QUEUED" + CheckRunStateSkipped CheckRunState = "SKIPPED" + CheckRunStateStale CheckRunState = "STALE" + CheckRunStateStartupFailure CheckRunState = "STARTUP_FAILURE" + CheckRunStateSuccess CheckRunState = "SUCCESS" + CheckRunStateTimedOut CheckRunState = "TIMED_OUT" + CheckRunStateWaiting CheckRunState = "WAITING" +) + +type CheckRunCountByState struct { + State CheckRunState + Count int +} + +// https://docs.github.com/en/graphql/reference/enums#statusstate +type StatusState string + +const ( + StatusStateError StatusState = "ERROR" + StatusStateExpected StatusState = "EXPECTED" + StatusStateFailure StatusState = "FAILURE" + StatusStatePending StatusState = "PENDING" + StatusStateSuccess StatusState = "SUCCESS" +) + +type StatusContextCountByState struct { + State StatusState + Count int +} + +// https://docs.github.com/en/graphql/reference/enums#checkstatusstate +type CheckStatusState string + +const ( + CheckStatusStateCompleted CheckStatusState = "COMPLETED" + CheckStatusStateInProgress CheckStatusState = "IN_PROGRESS" + CheckStatusStatePending CheckStatusState = "PENDING" + CheckStatusStateQueued CheckStatusState = "QUEUED" + CheckStatusStateRequested CheckStatusState = "REQUESTED" + CheckStatusStateWaiting CheckStatusState = "WAITING" +) + +// https://docs.github.com/en/graphql/reference/enums#checkconclusionstate +type CheckConclusionState string + +const ( + CheckConclusionStateActionRequired CheckConclusionState = "ACTION_REQUIRED" + CheckConclusionStateCancelled CheckConclusionState = "CANCELLED" + CheckConclusionStateFailure CheckConclusionState = "FAILURE" + CheckConclusionStateNeutral CheckConclusionState = "NEUTRAL" + CheckConclusionStateSkipped CheckConclusionState = "SKIPPED" + CheckConclusionStateStale CheckConclusionState = "STALE" + CheckConclusionStateStartupFailure CheckConclusionState = "STARTUP_FAILURE" + CheckConclusionStateSuccess CheckConclusionState = "SUCCESS" + CheckConclusionStateTimedOut CheckConclusionState = "TIMED_OUT" +) + +type CheckContexts struct { + // These fields are available on newer versions of the GraphQL API + // to support summary counts by state + CheckRunCount int + CheckRunCountsByState []CheckRunCountByState + StatusContextCount int + StatusContextCountsByState []StatusContextCountByState + + // These are available on older versions and provide more details + // required for checks + Nodes []CheckContext + PageInfo struct { + HasNextPage bool + EndCursor string + } +} + +type CheckContext struct { + TypeName string `json:"__typename"` + Name string `json:"name"` + IsRequired bool `json:"isRequired"` + CheckSuite CheckSuite `json:"checkSuite"` + // QUEUED IN_PROGRESS COMPLETED WAITING PENDING REQUESTED + Status string `json:"status"` + // ACTION_REQUIRED TIMED_OUT CANCELLED FAILURE SUCCESS NEUTRAL SKIPPED STARTUP_FAILURE STALE + Conclusion CheckConclusionState `json:"conclusion"` + StartedAt time.Time `json:"startedAt"` + CompletedAt time.Time `json:"completedAt"` + DetailsURL string `json:"detailsUrl"` + + /* StatusContext fields */ + Context string `json:"context"` + Description string `json:"description"` + // EXPECTED ERROR FAILURE PENDING SUCCESS + State StatusState `json:"state"` + TargetURL string `json:"targetUrl"` + CreatedAt time.Time `json:"createdAt"` +} + +type CheckSuite struct { + WorkflowRun WorkflowRun `json:"workflowRun"` +} + +type WorkflowRun struct { + Event string `json:"event"` + Workflow Workflow `json:"workflow"` +} + +type Workflow struct { + Name string `json:"name"` +} + +type PRRepository struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type AutoMergeRequest struct { + AuthorEmail *string `json:"authorEmail"` + CommitBody *string `json:"commitBody"` + CommitHeadline *string `json:"commitHeadline"` + // MERGE, REBASE, SQUASH + MergeMethod string `json:"mergeMethod"` + EnabledAt time.Time `json:"enabledAt"` + EnabledBy Author `json:"enabledBy"` +} + +// Commit loads just the commit SHA and nothing else +type Commit struct { + OID string `json:"oid"` +} + +type PullRequestCommit struct { + Commit PullRequestCommitCommit +} + +// PullRequestCommitCommit contains full information about a commit +type PullRequestCommitCommit struct { + OID string `json:"oid"` + Authors struct { + Nodes []struct { + Name string + Email string + User GitHubUser + } + } + MessageHeadline string + MessageBody string + CommittedDate time.Time + AuthoredDate time.Time +} + +type PullRequestFile struct { + Path string `json:"path"` + Additions int `json:"additions"` + Deletions int `json:"deletions"` +} + +type ReviewRequests struct { + Nodes []struct { + RequestedReviewer RequestedReviewer + } +} + +type RequestedReviewer struct { + TypeName string `json:"__typename"` + Login string `json:"login"` + Name string `json:"name"` + Slug string `json:"slug"` + Organization struct { + Login string `json:"login"` + } `json:"organization"` +} + +func (r RequestedReviewer) LoginOrSlug() string { + if r.TypeName == teamTypeName { + return fmt.Sprintf("%s/%s", r.Organization.Login, r.Slug) + } + return r.Login +} + +const teamTypeName = "Team" + +func (r ReviewRequests) Logins() []string { + logins := make([]string, len(r.Nodes)) + for i, r := range r.Nodes { + logins[i] = r.RequestedReviewer.LoginOrSlug() + } + return logins +} + +func (pr PullRequest) HeadLabel() string { + if pr.IsCrossRepository { + return fmt.Sprintf("%s:%s", pr.HeadRepositoryOwner.Login, pr.HeadRefName) + } + return pr.HeadRefName +} + +func (pr PullRequest) Link() string { + return pr.URL +} + +func (pr PullRequest) Identifier() string { + return pr.ID +} + +func (pr PullRequest) CurrentUserComments() []Comment { + return pr.Comments.CurrentUserComments() +} + +func (pr PullRequest) IsOpen() bool { + return pr.State == "OPEN" +} + +type PullRequestReviewStatus struct { + ChangesRequested bool + Approved bool + ReviewRequired bool +} + +func (pr *PullRequest) ReviewStatus() PullRequestReviewStatus { + var status PullRequestReviewStatus + switch pr.ReviewDecision { + case "CHANGES_REQUESTED": + status.ChangesRequested = true + case "APPROVED": + status.Approved = true + case "REVIEW_REQUIRED": + status.ReviewRequired = true + } + return status +} + +type PullRequestChecksStatus struct { + Pending int + Failing int + Passing int + Total int +} + +func (pr *PullRequest) ChecksStatus() PullRequestChecksStatus { + var summary PullRequestChecksStatus + + if len(pr.StatusCheckRollup.Nodes) == 0 { + return summary + } + + contexts := pr.StatusCheckRollup.Nodes[0].Commit.StatusCheckRollup.Contexts + + // If this commit has counts by state then we can summarise check status from those + if len(contexts.CheckRunCountsByState) != 0 && len(contexts.StatusContextCountsByState) != 0 { + summary.Total = contexts.CheckRunCount + contexts.StatusContextCount + for _, countByState := range contexts.CheckRunCountsByState { + switch parseCheckStatusFromCheckRunState(countByState.State) { + case passing: + summary.Passing += countByState.Count + case failing: + summary.Failing += countByState.Count + default: + summary.Pending += countByState.Count + } + } + + for _, countByState := range contexts.StatusContextCountsByState { + switch parseCheckStatusFromStatusState(countByState.State) { + case passing: + summary.Passing += countByState.Count + case failing: + summary.Failing += countByState.Count + default: + summary.Pending += countByState.Count + } + } + + return summary + } + + // If we don't have the counts by state, then we'll need to summarise by looking at the more detailed contexts + for _, c := range contexts.Nodes { + // Nodes are a discriminated union of CheckRun or StatusContext and we can match on + // the TypeName to narrow the type. + if c.TypeName == "CheckRun" { + // https://docs.github.com/en/graphql/reference/enums#checkstatusstate + // If the status is completed then we can check the conclusion field + if c.Status == "COMPLETED" { + switch parseCheckStatusFromCheckConclusionState(c.Conclusion) { + case passing: + summary.Passing++ + case failing: + summary.Failing++ + default: + summary.Pending++ + } + // otherwise we're in some form of pending state: + // "COMPLETED", "IN_PROGRESS", "PENDING", "QUEUED", "REQUESTED", "WAITING" or otherwise unknown + } else { + summary.Pending++ + } + + } else { // c.TypeName == StatusContext + switch parseCheckStatusFromStatusState(c.State) { + case passing: + summary.Passing++ + case failing: + summary.Failing++ + default: + summary.Pending++ + } + } + summary.Total++ + } + + return summary +} + +type checkStatus int + +const ( + passing checkStatus = iota + failing + pending +) + +func parseCheckStatusFromStatusState(state StatusState) checkStatus { + switch state { + case StatusStateSuccess: + return passing + case StatusStateFailure, StatusStateError: + return failing + case StatusStateExpected, StatusStatePending: + return pending + // Currently, we treat anything unknown as pending, which includes any future unknown + // states we might get back from the API. It might be interesting to do some work to add an additional + // unknown state. + default: + return pending + } +} + +func parseCheckStatusFromCheckRunState(state CheckRunState) checkStatus { + switch state { + case CheckRunStateNeutral, CheckRunStateSkipped, CheckRunStateSuccess: + return passing + case CheckRunStateActionRequired, CheckRunStateCancelled, CheckRunStateFailure, CheckRunStateTimedOut: + return failing + case CheckRunStateCompleted, CheckRunStateInProgress, CheckRunStatePending, CheckRunStateQueued, + CheckRunStateStale, CheckRunStateStartupFailure, CheckRunStateWaiting: + return pending + // Currently, we treat anything unknown as pending, which includes any future unknown + // states we might get back from the API. It might be interesting to do some work to add an additional + // unknown state. + default: + return pending + } +} + +func parseCheckStatusFromCheckConclusionState(state CheckConclusionState) checkStatus { + switch state { + case CheckConclusionStateNeutral, CheckConclusionStateSkipped, CheckConclusionStateSuccess: + return passing + case CheckConclusionStateActionRequired, CheckConclusionStateCancelled, CheckConclusionStateFailure, CheckConclusionStateTimedOut: + return failing + case CheckConclusionStateStale, CheckConclusionStateStartupFailure: + return pending + // Currently, we treat anything unknown as pending, which includes any future unknown + // states we might get back from the API. It might be interesting to do some work to add an additional + // unknown state. + default: + return pending + } +} + +func (pr *PullRequest) DisplayableReviews() PullRequestReviews { + published := []PullRequestReview{} + for _, prr := range pr.Reviews.Nodes { + //Dont display pending reviews + //Dont display commenting reviews without top level comment body + if prr.State != "PENDING" && !(prr.State == "COMMENTED" && prr.Body == "") { + published = append(published, prr) + } + } + return PullRequestReviews{Nodes: published, TotalCount: len(published)} +} + +// CreatePullRequest creates a pull request in a GitHub repository +func CreatePullRequest(client *Client, repo *Repository, params map[string]interface{}) (*PullRequest, error) { + query := ` + mutation PullRequestCreate($input: CreatePullRequestInput!) { + createPullRequest(input: $input) { + pullRequest { + id + url + } + } + }` + + inputParams := map[string]interface{}{ + "repositoryId": repo.ID, + } + for key, val := range params { + switch key { + case "title", "body", "draft", "baseRefName", "headRefName", "maintainerCanModify": + inputParams[key] = val + } + } + variables := map[string]interface{}{ + "input": inputParams, + } + + result := struct { + CreatePullRequest struct { + PullRequest PullRequest + } + }{} + + err := client.GraphQL(repo.RepoHost(), query, variables, &result) + if err != nil { + return nil, err + } + pr := &result.CreatePullRequest.PullRequest + + // metadata parameters aren't currently available in `createPullRequest`, + // but they are in `updatePullRequest` + updateParams := make(map[string]interface{}) + for key, val := range params { + switch key { + case "assigneeIds", "labelIds", "projectIds", "milestoneId": + if !isBlank(val) { + updateParams[key] = val + } + } + } + if len(updateParams) > 0 { + updateQuery := ` + mutation PullRequestCreateMetadata($input: UpdatePullRequestInput!) { + updatePullRequest(input: $input) { clientMutationId } + }` + updateParams["pullRequestId"] = pr.ID + variables := map[string]interface{}{ + "input": updateParams, + } + err := client.GraphQL(repo.RepoHost(), updateQuery, variables, &result) + if err != nil { + return pr, err + } + } + + // reviewers are requested in yet another additional mutation + reviewParams := make(map[string]interface{}) + if ids, ok := params["userReviewerIds"]; ok && !isBlank(ids) { + reviewParams["userIds"] = ids + } + if ids, ok := params["teamReviewerIds"]; ok && !isBlank(ids) { + reviewParams["teamIds"] = ids + } + + //TODO: How much work to extract this into own method and use for create and edit? + if len(reviewParams) > 0 { + reviewQuery := ` + mutation PullRequestCreateRequestReviews($input: RequestReviewsInput!) { + requestReviews(input: $input) { clientMutationId } + }` + reviewParams["pullRequestId"] = pr.ID + reviewParams["union"] = true + variables := map[string]interface{}{ + "input": reviewParams, + } + err := client.GraphQL(repo.RepoHost(), reviewQuery, variables, &result) + if err != nil { + return pr, err + } + } + + // projectsV2 are added in yet another mutation + projectV2Ids, ok := params["projectV2Ids"].([]string) + if ok { + projectItems := make(map[string]string, len(projectV2Ids)) + for _, p := range projectV2Ids { + projectItems[p] = pr.ID + } + err = UpdateProjectV2Items(client, repo, projectItems, nil) + if err != nil { + return pr, err + } + } + + return pr, nil +} + +func UpdatePullRequestReviews(client *Client, repo ghrepo.Interface, params githubv4.RequestReviewsInput) error { + var mutation struct { + RequestReviews struct { + PullRequest struct { + ID string + } + } `graphql:"requestReviews(input: $input)"` + } + variables := map[string]interface{}{"input": params} + err := client.Mutate(repo.RepoHost(), "PullRequestUpdateRequestReviews", &mutation, variables) + return err +} + +func isBlank(v interface{}) bool { + switch vv := v.(type) { + case string: + return vv == "" + case []string: + return len(vv) == 0 + default: + return true + } +} + +func PullRequestClose(httpClient *http.Client, repo ghrepo.Interface, prID string) error { + var mutation struct { + ClosePullRequest struct { + PullRequest struct { + ID githubv4.ID + } + } `graphql:"closePullRequest(input: $input)"` + } + + variables := map[string]interface{}{ + "input": githubv4.ClosePullRequestInput{ + PullRequestID: prID, + }, + } + + client := NewClientFromHTTP(httpClient) + return client.Mutate(repo.RepoHost(), "PullRequestClose", &mutation, variables) +} + +func PullRequestReopen(httpClient *http.Client, repo ghrepo.Interface, prID string) error { + var mutation struct { + ReopenPullRequest struct { + PullRequest struct { + ID githubv4.ID + } + } `graphql:"reopenPullRequest(input: $input)"` + } + + variables := map[string]interface{}{ + "input": githubv4.ReopenPullRequestInput{ + PullRequestID: prID, + }, + } + + client := NewClientFromHTTP(httpClient) + return client.Mutate(repo.RepoHost(), "PullRequestReopen", &mutation, variables) +} + +func PullRequestReady(client *Client, repo ghrepo.Interface, pr *PullRequest) error { + var mutation struct { + MarkPullRequestReadyForReview struct { + PullRequest struct { + ID githubv4.ID + } + } `graphql:"markPullRequestReadyForReview(input: $input)"` + } + + variables := map[string]interface{}{ + "input": githubv4.MarkPullRequestReadyForReviewInput{ + PullRequestID: pr.ID, + }, + } + + return client.Mutate(repo.RepoHost(), "PullRequestReadyForReview", &mutation, variables) +} + +func ConvertPullRequestToDraft(client *Client, repo ghrepo.Interface, pr *PullRequest) error { + var mutation struct { + ConvertPullRequestToDraft struct { + PullRequest struct { + ID githubv4.ID + } + } `graphql:"convertPullRequestToDraft(input: $input)"` + } + + variables := map[string]interface{}{ + "input": githubv4.ConvertPullRequestToDraftInput{ + PullRequestID: pr.ID, + }, + } + + return client.Mutate(repo.RepoHost(), "ConvertPullRequestToDraft", &mutation, variables) +} + +func BranchDeleteRemote(client *Client, repo ghrepo.Interface, branch string) error { + path := fmt.Sprintf("repos/%s/%s/git/refs/heads/%s", repo.RepoOwner(), repo.RepoName(), url.PathEscape(branch)) + return client.REST(repo.RepoHost(), "DELETE", path, nil, nil) +} diff --git a/vendor/github.com/cli/cli/v2/api/queries_pr_review.go b/vendor/github.com/cli/cli/v2/api/queries_pr_review.go new file mode 100644 index 000000000..d5565b54b --- /dev/null +++ b/vendor/github.com/cli/cli/v2/api/queries_pr_review.go @@ -0,0 +1,117 @@ +package api + +import ( + "time" + + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/shurcooL/githubv4" +) + +type PullRequestReviewState int + +const ( + ReviewApprove PullRequestReviewState = iota + ReviewRequestChanges + ReviewComment +) + +type PullRequestReviewInput struct { + Body string + State PullRequestReviewState +} + +type PullRequestReviews struct { + Nodes []PullRequestReview + PageInfo struct { + HasNextPage bool + EndCursor string + } + TotalCount int +} + +type PullRequestReview struct { + ID string `json:"id"` + Author CommentAuthor `json:"author"` + AuthorAssociation string `json:"authorAssociation"` + Body string `json:"body"` + SubmittedAt *time.Time `json:"submittedAt"` + IncludesCreatedEdit bool `json:"includesCreatedEdit"` + ReactionGroups ReactionGroups `json:"reactionGroups"` + State string `json:"state"` + URL string `json:"url,omitempty"` + Commit Commit `json:"commit"` +} + +func AddReview(client *Client, repo ghrepo.Interface, pr *PullRequest, input *PullRequestReviewInput) error { + var mutation struct { + AddPullRequestReview struct { + ClientMutationID string + } `graphql:"addPullRequestReview(input:$input)"` + } + + state := githubv4.PullRequestReviewEventComment + switch input.State { + case ReviewApprove: + state = githubv4.PullRequestReviewEventApprove + case ReviewRequestChanges: + state = githubv4.PullRequestReviewEventRequestChanges + } + + body := githubv4.String(input.Body) + variables := map[string]interface{}{ + "input": githubv4.AddPullRequestReviewInput{ + PullRequestID: pr.ID, + Event: &state, + Body: &body, + }, + } + + return client.Mutate(repo.RepoHost(), "PullRequestReviewAdd", &mutation, variables) +} + +func (prr PullRequestReview) Identifier() string { + return prr.ID +} + +func (prr PullRequestReview) AuthorLogin() string { + return prr.Author.Login +} + +func (prr PullRequestReview) Association() string { + return prr.AuthorAssociation +} + +func (prr PullRequestReview) Content() string { + return prr.Body +} + +func (prr PullRequestReview) Created() time.Time { + if prr.SubmittedAt == nil { + return time.Time{} + } + return *prr.SubmittedAt +} + +func (prr PullRequestReview) HiddenReason() string { + return "" +} + +func (prr PullRequestReview) IsEdited() bool { + return prr.IncludesCreatedEdit +} + +func (prr PullRequestReview) IsHidden() bool { + return false +} + +func (prr PullRequestReview) Link() string { + return prr.URL +} + +func (prr PullRequestReview) Reactions() ReactionGroups { + return prr.ReactionGroups +} + +func (prr PullRequestReview) Status() string { + return prr.State +} diff --git a/vendor/github.com/cli/cli/v2/api/queries_projects_v2.go b/vendor/github.com/cli/cli/v2/api/queries_projects_v2.go new file mode 100644 index 000000000..958be6616 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/api/queries_projects_v2.go @@ -0,0 +1,330 @@ +package api + +import ( + "fmt" + "strings" + + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/shurcooL/githubv4" +) + +const ( + errorProjectsV2ReadScope = "field requires one of the following scopes: ['read:project']" + errorProjectsV2UserField = "Field 'projectsV2' doesn't exist on type 'User'" + errorProjectsV2RepositoryField = "Field 'projectsV2' doesn't exist on type 'Repository'" + errorProjectsV2OrganizationField = "Field 'projectsV2' doesn't exist on type 'Organization'" + errorProjectsV2IssueField = "Field 'projectItems' doesn't exist on type 'Issue'" + errorProjectsV2PullRequestField = "Field 'projectItems' doesn't exist on type 'PullRequest'" +) + +type ProjectV2 struct { + ID string `json:"id"` + Title string `json:"title"` + Number int `json:"number"` + ResourcePath string `json:"resourcePath"` + Closed bool `json:"closed"` +} + +// UpdateProjectV2Items uses the addProjectV2ItemById and the deleteProjectV2Item mutations +// to add and delete items from projects. The addProjectItems and deleteProjectItems arguments are +// mappings between a project and an item. This function can be used across multiple projects +// and items. Note that the deleteProjectV2Item mutation requires the item id from the project not +// the global id. +func UpdateProjectV2Items(client *Client, repo ghrepo.Interface, addProjectItems, deleteProjectItems map[string]string) error { + l := len(addProjectItems) + len(deleteProjectItems) + if l == 0 { + return nil + } + inputs := make([]string, 0, l) + mutations := make([]string, 0, l) + variables := make(map[string]interface{}, l) + var i int + + for project, item := range addProjectItems { + inputs = append(inputs, fmt.Sprintf("$input_%03d: AddProjectV2ItemByIdInput!", i)) + mutations = append(mutations, fmt.Sprintf("add_%03d: addProjectV2ItemById(input: $input_%03d) { item { id } }", i, i)) + variables[fmt.Sprintf("input_%03d", i)] = map[string]interface{}{"contentId": item, "projectId": project} + i++ + } + + for project, item := range deleteProjectItems { + inputs = append(inputs, fmt.Sprintf("$input_%03d: DeleteProjectV2ItemInput!", i)) + mutations = append(mutations, fmt.Sprintf("delete_%03d: deleteProjectV2Item(input: $input_%03d) { deletedItemId }", i, i)) + variables[fmt.Sprintf("input_%03d", i)] = map[string]interface{}{"itemId": item, "projectId": project} + i++ + } + + query := fmt.Sprintf(`mutation UpdateProjectV2Items(%s) {%s}`, strings.Join(inputs, " "), strings.Join(mutations, " ")) + + return client.GraphQL(repo.RepoHost(), query, variables, nil) +} + +// ProjectsV2ItemsForIssue fetches all ProjectItems for an issue. +func ProjectsV2ItemsForIssue(client *Client, repo ghrepo.Interface, issue *Issue) error { + type projectV2ItemStatus struct { + StatusFragment struct { + OptionID string `json:"optionId"` + Name string `json:"name"` + } `graphql:"... on ProjectV2ItemFieldSingleSelectValue"` + } + + type projectV2Item struct { + ID string `json:"id"` + Project struct { + ID string `json:"id"` + Title string `json:"title"` + } + Status projectV2ItemStatus `graphql:"status:fieldValueByName(name: \"Status\")"` + } + + type response struct { + Repository struct { + Issue struct { + ProjectItems struct { + Nodes []*projectV2Item + PageInfo struct { + HasNextPage bool + EndCursor string + } + } `graphql:"projectItems(first: 100, after: $endCursor)"` + } `graphql:"issue(number: $number)"` + } `graphql:"repository(owner: $owner, name: $name)"` + } + variables := map[string]interface{}{ + "owner": githubv4.String(repo.RepoOwner()), + "name": githubv4.String(repo.RepoName()), + "number": githubv4.Int(issue.Number), + "endCursor": (*githubv4.String)(nil), + } + var items ProjectItems + for { + var query response + err := client.Query(repo.RepoHost(), "IssueProjectItems", &query, variables) + if err != nil { + return err + } + for _, projectItemNode := range query.Repository.Issue.ProjectItems.Nodes { + items.Nodes = append(items.Nodes, &ProjectV2Item{ + ID: projectItemNode.ID, + Project: ProjectV2ItemProject{ + ID: projectItemNode.Project.ID, + Title: projectItemNode.Project.Title, + }, + Status: ProjectV2ItemStatus{ + OptionID: projectItemNode.Status.StatusFragment.OptionID, + Name: projectItemNode.Status.StatusFragment.Name, + }, + }) + } + + if !query.Repository.Issue.ProjectItems.PageInfo.HasNextPage { + break + } + variables["endCursor"] = githubv4.String(query.Repository.Issue.ProjectItems.PageInfo.EndCursor) + } + issue.ProjectItems = items + return nil +} + +// ProjectsV2ItemsForPullRequest fetches all ProjectItems for a pull request. +func ProjectsV2ItemsForPullRequest(client *Client, repo ghrepo.Interface, pr *PullRequest) error { + type projectV2ItemStatus struct { + StatusFragment struct { + OptionID string `json:"optionId"` + Name string `json:"name"` + } `graphql:"... on ProjectV2ItemFieldSingleSelectValue"` + } + + type projectV2Item struct { + ID string `json:"id"` + Project struct { + ID string `json:"id"` + Title string `json:"title"` + } + Status projectV2ItemStatus `graphql:"status:fieldValueByName(name: \"Status\")"` + } + + type response struct { + Repository struct { + PullRequest struct { + ProjectItems struct { + Nodes []*projectV2Item + PageInfo struct { + HasNextPage bool + EndCursor string + } + } `graphql:"projectItems(first: 100, after: $endCursor)"` + } `graphql:"pullRequest(number: $number)"` + } `graphql:"repository(owner: $owner, name: $name)"` + } + variables := map[string]interface{}{ + "owner": githubv4.String(repo.RepoOwner()), + "name": githubv4.String(repo.RepoName()), + "number": githubv4.Int(pr.Number), + "endCursor": (*githubv4.String)(nil), + } + var items ProjectItems + for { + var query response + err := client.Query(repo.RepoHost(), "PullRequestProjectItems", &query, variables) + if err != nil { + return err + } + + for _, projectItemNode := range query.Repository.PullRequest.ProjectItems.Nodes { + items.Nodes = append(items.Nodes, &ProjectV2Item{ + ID: projectItemNode.ID, + Project: ProjectV2ItemProject{ + ID: projectItemNode.Project.ID, + Title: projectItemNode.Project.Title, + }, + Status: ProjectV2ItemStatus{ + OptionID: projectItemNode.Status.StatusFragment.OptionID, + Name: projectItemNode.Status.StatusFragment.Name, + }, + }) + } + + if !query.Repository.PullRequest.ProjectItems.PageInfo.HasNextPage { + break + } + variables["endCursor"] = githubv4.String(query.Repository.PullRequest.ProjectItems.PageInfo.EndCursor) + } + pr.ProjectItems = items + return nil +} + +// OrganizationProjectsV2 fetches all open projectsV2 for an organization. +func OrganizationProjectsV2(client *Client, repo ghrepo.Interface) ([]ProjectV2, error) { + type responseData struct { + Organization struct { + ProjectsV2 struct { + Nodes []ProjectV2 + PageInfo struct { + HasNextPage bool + EndCursor string + } + } `graphql:"projectsV2(first: 100, orderBy: {field: TITLE, direction: ASC}, after: $endCursor, query: $query)"` + } `graphql:"organization(login: $owner)"` + } + + variables := map[string]interface{}{ + "owner": githubv4.String(repo.RepoOwner()), + "endCursor": (*githubv4.String)(nil), + "query": githubv4.String("is:open"), + } + + var projectsV2 []ProjectV2 + for { + var query responseData + err := client.Query(repo.RepoHost(), "OrganizationProjectV2List", &query, variables) + if err != nil { + return nil, err + } + + projectsV2 = append(projectsV2, query.Organization.ProjectsV2.Nodes...) + + if !query.Organization.ProjectsV2.PageInfo.HasNextPage { + break + } + variables["endCursor"] = githubv4.String(query.Organization.ProjectsV2.PageInfo.EndCursor) + } + + return projectsV2, nil +} + +// RepoProjectsV2 fetches all open projectsV2 for a repository. +func RepoProjectsV2(client *Client, repo ghrepo.Interface) ([]ProjectV2, error) { + type responseData struct { + Repository struct { + ProjectsV2 struct { + Nodes []ProjectV2 + PageInfo struct { + HasNextPage bool + EndCursor string + } + } `graphql:"projectsV2(first: 100, orderBy: {field: TITLE, direction: ASC}, after: $endCursor, query: $query)"` + } `graphql:"repository(owner: $owner, name: $name)"` + } + + variables := map[string]interface{}{ + "owner": githubv4.String(repo.RepoOwner()), + "name": githubv4.String(repo.RepoName()), + "endCursor": (*githubv4.String)(nil), + "query": githubv4.String("is:open"), + } + + var projectsV2 []ProjectV2 + for { + var query responseData + err := client.Query(repo.RepoHost(), "RepositoryProjectV2List", &query, variables) + if err != nil { + return nil, err + } + + projectsV2 = append(projectsV2, query.Repository.ProjectsV2.Nodes...) + + if !query.Repository.ProjectsV2.PageInfo.HasNextPage { + break + } + variables["endCursor"] = githubv4.String(query.Repository.ProjectsV2.PageInfo.EndCursor) + } + + return projectsV2, nil +} + +// CurrentUserProjectsV2 fetches all open projectsV2 for current user. +func CurrentUserProjectsV2(client *Client, hostname string) ([]ProjectV2, error) { + type responseData struct { + Viewer struct { + ProjectsV2 struct { + Nodes []ProjectV2 + PageInfo struct { + HasNextPage bool + EndCursor string + } + } `graphql:"projectsV2(first: 100, orderBy: {field: TITLE, direction: ASC}, after: $endCursor, query: $query)"` + } `graphql:"viewer"` + } + + variables := map[string]interface{}{ + "endCursor": (*githubv4.String)(nil), + "query": githubv4.String("is:open"), + } + + var projectsV2 []ProjectV2 + for { + var query responseData + err := client.Query(hostname, "UserProjectV2List", &query, variables) + if err != nil { + return nil, err + } + + projectsV2 = append(projectsV2, query.Viewer.ProjectsV2.Nodes...) + + if !query.Viewer.ProjectsV2.PageInfo.HasNextPage { + break + } + variables["endCursor"] = githubv4.String(query.Viewer.ProjectsV2.PageInfo.EndCursor) + } + + return projectsV2, nil +} + +// When querying ProjectsV2 fields we generally dont want to show the user +// scope errors and field does not exist errors. ProjectsV2IgnorableError +// checks against known error strings to see if an error can be safely ignored. +// Due to the fact that the GraphQLClient can return multiple types of errors +// this uses brittle string comparison to check against the known error strings. +func ProjectsV2IgnorableError(err error) bool { + msg := err.Error() + if strings.Contains(msg, errorProjectsV2ReadScope) || + strings.Contains(msg, errorProjectsV2UserField) || + strings.Contains(msg, errorProjectsV2RepositoryField) || + strings.Contains(msg, errorProjectsV2OrganizationField) || + strings.Contains(msg, errorProjectsV2IssueField) || + strings.Contains(msg, errorProjectsV2PullRequestField) { + return true + } + return false +} diff --git a/vendor/github.com/cli/cli/v2/api/queries_repo.go b/vendor/github.com/cli/cli/v2/api/queries_repo.go new file mode 100644 index 000000000..01f85354f --- /dev/null +++ b/vendor/github.com/cli/cli/v2/api/queries_repo.go @@ -0,0 +1,1390 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "sort" + "strings" + "time" + + "github.com/cli/cli/v2/internal/ghinstance" + "golang.org/x/sync/errgroup" + + "github.com/cli/cli/v2/internal/ghrepo" + ghAPI "github.com/cli/go-gh/v2/pkg/api" + "github.com/shurcooL/githubv4" +) + +const ( + errorResolvingOrganization = "Could not resolve to an Organization" +) + +// Repository contains information about a GitHub repo +type Repository struct { + ID string + Name string + NameWithOwner string + Owner RepositoryOwner + Parent *Repository + TemplateRepository *Repository + Description string + HomepageURL string + OpenGraphImageURL string + UsesCustomOpenGraphImage bool + URL string + SSHURL string + MirrorURL string + SecurityPolicyURL string + + CreatedAt time.Time + PushedAt *time.Time + UpdatedAt time.Time + + IsBlankIssuesEnabled bool + IsSecurityPolicyEnabled bool + HasIssuesEnabled bool + HasProjectsEnabled bool + HasDiscussionsEnabled bool + HasWikiEnabled bool + MergeCommitAllowed bool + SquashMergeAllowed bool + RebaseMergeAllowed bool + AutoMergeAllowed bool + + ForkCount int + StargazerCount int + Watchers struct { + TotalCount int `json:"totalCount"` + } + Issues struct { + TotalCount int `json:"totalCount"` + } + PullRequests struct { + TotalCount int `json:"totalCount"` + } + + CodeOfConduct *CodeOfConduct + ContactLinks []ContactLink + DefaultBranchRef BranchRef + DeleteBranchOnMerge bool + DiskUsage int + FundingLinks []FundingLink + IsArchived bool + IsEmpty bool + IsFork bool + ForkingAllowed bool + IsInOrganization bool + IsMirror bool + IsPrivate bool + IsTemplate bool + IsUserConfigurationRepository bool + LicenseInfo *RepositoryLicense + ViewerCanAdminister bool + ViewerDefaultCommitEmail string + ViewerDefaultMergeMethod string + ViewerHasStarred bool + ViewerPermission string + ViewerPossibleCommitEmails []string + ViewerSubscription string + Visibility string + + RepositoryTopics struct { + Nodes []struct { + Topic RepositoryTopic + } + } + PrimaryLanguage *CodingLanguage + Languages struct { + Edges []struct { + Size int `json:"size"` + Node CodingLanguage `json:"node"` + } + } + IssueTemplates []IssueTemplate + PullRequestTemplates []PullRequestTemplate + Labels struct { + Nodes []IssueLabel + } + Milestones struct { + Nodes []Milestone + } + LatestRelease *RepositoryRelease + + AssignableUsers struct { + Nodes []GitHubUser + } + MentionableUsers struct { + Nodes []GitHubUser + } + Projects struct { + Nodes []RepoProject + } + + // pseudo-field that keeps track of host name of this repo + hostname string +} + +// RepositoryOwner is the owner of a GitHub repository +type RepositoryOwner struct { + ID string `json:"id"` + Login string `json:"login"` +} + +type GitHubUser struct { + ID string `json:"id"` + Login string `json:"login"` + Name string `json:"name"` +} + +// BranchRef is the branch name in a GitHub repository +type BranchRef struct { + Name string `json:"name"` +} + +type CodeOfConduct struct { + Key string `json:"key"` + Name string `json:"name"` + URL string `json:"url"` +} + +type RepositoryLicense struct { + Key string `json:"key"` + Name string `json:"name"` + Nickname string `json:"nickname"` +} + +type ContactLink struct { + About string `json:"about"` + Name string `json:"name"` + URL string `json:"url"` +} + +type FundingLink struct { + Platform string `json:"platform"` + URL string `json:"url"` +} + +type CodingLanguage struct { + Name string `json:"name"` +} + +type IssueTemplate struct { + Name string `json:"name"` + Title string `json:"title"` + Body string `json:"body"` + About string `json:"about"` +} + +type PullRequestTemplate struct { + Filename string `json:"filename"` + Body string `json:"body"` +} + +type RepositoryTopic struct { + Name string `json:"name"` +} + +type RepositoryRelease struct { + Name string `json:"name"` + TagName string `json:"tagName"` + URL string `json:"url"` + PublishedAt time.Time `json:"publishedAt"` +} + +type IssueLabel struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Color string `json:"color"` +} + +type License struct { + Key string `json:"key"` + Name string `json:"name"` +} + +// RepoOwner is the login name of the owner +func (r Repository) RepoOwner() string { + return r.Owner.Login +} + +// RepoName is the name of the repository +func (r Repository) RepoName() string { + return r.Name +} + +// RepoHost is the GitHub hostname of the repository +func (r Repository) RepoHost() string { + return r.hostname +} + +// ViewerCanPush is true when the requesting user has push access +func (r Repository) ViewerCanPush() bool { + switch r.ViewerPermission { + case "ADMIN", "MAINTAIN", "WRITE": + return true + default: + return false + } +} + +// ViewerCanTriage is true when the requesting user can triage issues and pull requests +func (r Repository) ViewerCanTriage() bool { + switch r.ViewerPermission { + case "ADMIN", "MAINTAIN", "WRITE", "TRIAGE": + return true + default: + return false + } +} + +func FetchRepository(client *Client, repo ghrepo.Interface, fields []string) (*Repository, error) { + query := fmt.Sprintf(`query RepositoryInfo($owner: String!, $name: String!) { + repository(owner: $owner, name: $name) {%s} + }`, RepositoryGraphQL(fields)) + + variables := map[string]interface{}{ + "owner": repo.RepoOwner(), + "name": repo.RepoName(), + } + + var result struct { + Repository *Repository + } + if err := client.GraphQL(repo.RepoHost(), query, variables, &result); err != nil { + return nil, err + } + // The GraphQL API should have returned an error in case of a missing repository, but this isn't + // guaranteed to happen when an authentication token with insufficient permissions is being used. + if result.Repository == nil { + return nil, GraphQLError{ + GraphQLError: &ghAPI.GraphQLError{ + Errors: []ghAPI.GraphQLErrorItem{{ + Type: "NOT_FOUND", + Message: fmt.Sprintf("Could not resolve to a Repository with the name '%s/%s'.", repo.RepoOwner(), repo.RepoName()), + }}, + }, + } + } + + return InitRepoHostname(result.Repository, repo.RepoHost()), nil +} + +func GitHubRepo(client *Client, repo ghrepo.Interface) (*Repository, error) { + query := ` + fragment repo on Repository { + id + name + owner { login } + hasIssuesEnabled + description + hasWikiEnabled + viewerPermission + defaultBranchRef { + name + } + } + + query RepositoryInfo($owner: String!, $name: String!) { + repository(owner: $owner, name: $name) { + ...repo + parent { + ...repo + } + mergeCommitAllowed + rebaseMergeAllowed + squashMergeAllowed + } + }` + variables := map[string]interface{}{ + "owner": repo.RepoOwner(), + "name": repo.RepoName(), + } + + var result struct { + Repository *Repository + } + if err := client.GraphQL(repo.RepoHost(), query, variables, &result); err != nil { + return nil, err + } + // The GraphQL API should have returned an error in case of a missing repository, but this isn't + // guaranteed to happen when an authentication token with insufficient permissions is being used. + if result.Repository == nil { + return nil, GraphQLError{ + GraphQLError: &ghAPI.GraphQLError{ + Errors: []ghAPI.GraphQLErrorItem{{ + Type: "NOT_FOUND", + Message: fmt.Sprintf("Could not resolve to a Repository with the name '%s/%s'.", repo.RepoOwner(), repo.RepoName()), + }}, + }, + } + } + + return InitRepoHostname(result.Repository, repo.RepoHost()), nil +} + +func RepoDefaultBranch(client *Client, repo ghrepo.Interface) (string, error) { + if r, ok := repo.(*Repository); ok && r.DefaultBranchRef.Name != "" { + return r.DefaultBranchRef.Name, nil + } + + r, err := GitHubRepo(client, repo) + if err != nil { + return "", err + } + return r.DefaultBranchRef.Name, nil +} + +func CanPushToRepo(httpClient *http.Client, repo ghrepo.Interface) (bool, error) { + if r, ok := repo.(*Repository); ok && r.ViewerPermission != "" { + return r.ViewerCanPush(), nil + } + + apiClient := NewClientFromHTTP(httpClient) + r, err := GitHubRepo(apiClient, repo) + if err != nil { + return false, err + } + return r.ViewerCanPush(), nil +} + +// RepoParent finds out the parent repository of a fork +func RepoParent(client *Client, repo ghrepo.Interface) (ghrepo.Interface, error) { + var query struct { + Repository struct { + Parent *struct { + Name string + Owner struct { + Login string + } + } + } `graphql:"repository(owner: $owner, name: $name)"` + } + + variables := map[string]interface{}{ + "owner": githubv4.String(repo.RepoOwner()), + "name": githubv4.String(repo.RepoName()), + } + + err := client.Query(repo.RepoHost(), "RepositoryFindParent", &query, variables) + if err != nil { + return nil, err + } + if query.Repository.Parent == nil { + return nil, nil + } + + parent := ghrepo.NewWithHost(query.Repository.Parent.Owner.Login, query.Repository.Parent.Name, repo.RepoHost()) + return parent, nil +} + +// RepoNetworkResult describes the relationship between related repositories +type RepoNetworkResult struct { + ViewerLogin string + Repositories []*Repository +} + +// RepoNetwork inspects the relationship between multiple GitHub repositories +func RepoNetwork(client *Client, repos []ghrepo.Interface) (RepoNetworkResult, error) { + var hostname string + if len(repos) > 0 { + hostname = repos[0].RepoHost() + } + + queries := make([]string, 0, len(repos)) + for i, repo := range repos { + queries = append(queries, fmt.Sprintf(` + repo_%03d: repository(owner: %q, name: %q) { + ...repo + parent { + ...repo + } + } + `, i, repo.RepoOwner(), repo.RepoName())) + } + + // Since the query is constructed dynamically, we can't parse a response + // format using a static struct. Instead, hold the raw JSON data until we + // decide how to parse it manually. + graphqlResult := make(map[string]*json.RawMessage) + var result RepoNetworkResult + + err := client.GraphQL(hostname, fmt.Sprintf(` + fragment repo on Repository { + id + name + owner { login } + viewerPermission + defaultBranchRef { + name + } + isPrivate + } + query RepositoryNetwork { + viewer { login } + %s + } + `, strings.Join(queries, "")), nil, &graphqlResult) + var graphqlError GraphQLError + if errors.As(err, &graphqlError) { + // If the only errors are that certain repositories are not found, + // continue processing this response instead of returning an error + tolerated := true + for _, ge := range graphqlError.Errors { + if ge.Type != "NOT_FOUND" { + tolerated = false + } + } + if tolerated { + err = nil + } + } + if err != nil { + return result, err + } + + keys := make([]string, 0, len(graphqlResult)) + for key := range graphqlResult { + keys = append(keys, key) + } + // sort keys to ensure `repo_{N}` entries are processed in order + sort.Strings(keys) + + // Iterate over keys of GraphQL response data and, based on its name, + // dynamically allocate the target struct an individual message gets decoded to. + for _, name := range keys { + jsonMessage := graphqlResult[name] + if name == "viewer" { + viewerResult := struct { + Login string + }{} + decoder := json.NewDecoder(bytes.NewReader([]byte(*jsonMessage))) + if err := decoder.Decode(&viewerResult); err != nil { + return result, err + } + result.ViewerLogin = viewerResult.Login + } else if strings.HasPrefix(name, "repo_") { + if jsonMessage == nil { + result.Repositories = append(result.Repositories, nil) + continue + } + var repo Repository + decoder := json.NewDecoder(bytes.NewReader(*jsonMessage)) + if err := decoder.Decode(&repo); err != nil { + return result, err + } + result.Repositories = append(result.Repositories, InitRepoHostname(&repo, hostname)) + } else { + return result, fmt.Errorf("unknown GraphQL result key %q", name) + } + } + return result, nil +} + +func InitRepoHostname(repo *Repository, hostname string) *Repository { + repo.hostname = hostname + if repo.Parent != nil { + repo.Parent.hostname = hostname + } + return repo +} + +// RepositoryV3 is the repository result from GitHub API v3 +type repositoryV3 struct { + NodeID string `json:"node_id"` + Name string + CreatedAt time.Time `json:"created_at"` + Owner struct { + Login string + } + Private bool + HTMLUrl string `json:"html_url"` + Parent *repositoryV3 +} + +// ForkRepo forks the repository on GitHub and returns the new repository +func ForkRepo(client *Client, repo ghrepo.Interface, org, newName string, defaultBranchOnly bool) (*Repository, error) { + path := fmt.Sprintf("repos/%s/forks", ghrepo.FullName(repo)) + + params := map[string]interface{}{} + if org != "" { + params["organization"] = org + } + if newName != "" { + params["name"] = newName + } + if defaultBranchOnly { + params["default_branch_only"] = true + } + + body := &bytes.Buffer{} + enc := json.NewEncoder(body) + if err := enc.Encode(params); err != nil { + return nil, err + } + + result := repositoryV3{} + err := client.REST(repo.RepoHost(), "POST", path, body, &result) + if err != nil { + return nil, err + } + + newRepo := &Repository{ + ID: result.NodeID, + Name: result.Name, + CreatedAt: result.CreatedAt, + Owner: RepositoryOwner{ + Login: result.Owner.Login, + }, + ViewerPermission: "WRITE", + hostname: repo.RepoHost(), + } + + // The GitHub API will happily return a HTTP 200 when attempting to fork own repo even though no forking + // actually took place. Ensure that we raise an error instead. + if ghrepo.IsSame(repo, newRepo) { + return newRepo, fmt.Errorf("%s cannot be forked. A single user account cannot own both a parent and fork.", ghrepo.FullName(repo)) + } + + return newRepo, nil +} + +// RenameRepo renames the repository on GitHub and returns the renamed repository +func RenameRepo(client *Client, repo ghrepo.Interface, newRepoName string) (*Repository, error) { + input := map[string]string{"name": newRepoName} + body := &bytes.Buffer{} + enc := json.NewEncoder(body) + if err := enc.Encode(input); err != nil { + return nil, err + } + + path := fmt.Sprintf("%srepos/%s", + ghinstance.RESTPrefix(repo.RepoHost()), + ghrepo.FullName(repo)) + + result := repositoryV3{} + err := client.REST(repo.RepoHost(), "PATCH", path, body, &result) + if err != nil { + return nil, err + } + + return &Repository{ + ID: result.NodeID, + Name: result.Name, + CreatedAt: result.CreatedAt, + Owner: RepositoryOwner{ + Login: result.Owner.Login, + }, + ViewerPermission: "WRITE", + hostname: repo.RepoHost(), + }, nil +} + +func LastCommit(client *Client, repo ghrepo.Interface) (*Commit, error) { + var responseData struct { + Repository struct { + DefaultBranchRef struct { + Target struct { + Commit `graphql:"... on Commit"` + } + } + } `graphql:"repository(owner: $owner, name: $repo)"` + } + variables := map[string]interface{}{ + "owner": githubv4.String(repo.RepoOwner()), "repo": githubv4.String(repo.RepoName()), + } + if err := client.Query(repo.RepoHost(), "LastCommit", &responseData, variables); err != nil { + return nil, err + } + return &responseData.Repository.DefaultBranchRef.Target.Commit, nil +} + +// RepoFindForks finds forks of the repo that are affiliated with the viewer +func RepoFindForks(client *Client, repo ghrepo.Interface, limit int) ([]*Repository, error) { + result := struct { + Repository struct { + Forks struct { + Nodes []Repository + } + } + }{} + + variables := map[string]interface{}{ + "owner": repo.RepoOwner(), + "repo": repo.RepoName(), + "limit": limit, + } + + if err := client.GraphQL(repo.RepoHost(), ` + query RepositoryFindFork($owner: String!, $repo: String!, $limit: Int!) { + repository(owner: $owner, name: $repo) { + forks(first: $limit, affiliations: [OWNER, COLLABORATOR]) { + nodes { + id + name + owner { login } + url + viewerPermission + } + } + } + } + `, variables, &result); err != nil { + return nil, err + } + + var results []*Repository + for _, r := range result.Repository.Forks.Nodes { + // we check ViewerCanPush, even though we expect it to always be true per + // `affiliations` condition, to guard against versions of GitHub with a + // faulty `affiliations` implementation + if !r.ViewerCanPush() { + continue + } + results = append(results, InitRepoHostname(&r, repo.RepoHost())) + } + + return results, nil +} + +type RepoMetadataResult struct { + CurrentLogin string + AssignableUsers []RepoAssignee + Labels []RepoLabel + Projects []RepoProject + ProjectsV2 []ProjectV2 + Milestones []RepoMilestone + Teams []OrgTeam +} + +func (m *RepoMetadataResult) MembersToIDs(names []string) ([]string, error) { + var ids []string + for _, assigneeLogin := range names { + found := false + for _, u := range m.AssignableUsers { + if strings.EqualFold(assigneeLogin, u.Login) { + ids = append(ids, u.ID) + found = true + break + } + } + if !found { + return nil, fmt.Errorf("'%s' not found", assigneeLogin) + } + } + return ids, nil +} + +func (m *RepoMetadataResult) TeamsToIDs(names []string) ([]string, error) { + var ids []string + for _, teamSlug := range names { + found := false + slug := teamSlug[strings.IndexRune(teamSlug, '/')+1:] + for _, t := range m.Teams { + if strings.EqualFold(slug, t.Slug) { + ids = append(ids, t.ID) + found = true + break + } + } + if !found { + return nil, fmt.Errorf("'%s' not found", teamSlug) + } + } + return ids, nil +} + +func (m *RepoMetadataResult) LabelsToIDs(names []string) ([]string, error) { + var ids []string + for _, labelName := range names { + found := false + for _, l := range m.Labels { + if strings.EqualFold(labelName, l.Name) { + ids = append(ids, l.ID) + found = true + break + } + } + if !found { + return nil, fmt.Errorf("'%s' not found", labelName) + } + } + return ids, nil +} + +// ProjectsToIDs returns two arrays: +// - the first contains IDs of projects V1 +// - the second contains IDs of projects V2 +// - if neither project V1 or project V2 can be found with a given name, then an error is returned +func (m *RepoMetadataResult) ProjectsToIDs(names []string) ([]string, []string, error) { + var ids []string + var idsV2 []string + for _, projectName := range names { + id, found := m.projectNameToID(projectName) + if found { + ids = append(ids, id) + continue + } + + idV2, found := m.projectV2TitleToID(projectName) + if found { + idsV2 = append(idsV2, idV2) + continue + } + + return nil, nil, fmt.Errorf("'%s' not found", projectName) + } + return ids, idsV2, nil +} + +func (m *RepoMetadataResult) projectNameToID(projectName string) (string, bool) { + for _, p := range m.Projects { + if strings.EqualFold(projectName, p.Name) { + return p.ID, true + } + } + + return "", false +} + +func (m *RepoMetadataResult) projectV2TitleToID(projectTitle string) (string, bool) { + for _, p := range m.ProjectsV2 { + if strings.EqualFold(projectTitle, p.Title) { + return p.ID, true + } + } + + return "", false +} + +func ProjectsToPaths(projects []RepoProject, projectsV2 []ProjectV2, names []string) ([]string, error) { + var paths []string + for _, projectName := range names { + found := false + for _, p := range projects { + if strings.EqualFold(projectName, p.Name) { + // format of ResourcePath: /OWNER/REPO/projects/PROJECT_NUMBER or /orgs/ORG/projects/PROJECT_NUMBER or /users/USER/projects/PROJECT_NUBER + // required format of path: OWNER/REPO/PROJECT_NUMBER or ORG/PROJECT_NUMBER or USER/PROJECT_NUMBER + var path string + pathParts := strings.Split(p.ResourcePath, "/") + if pathParts[1] == "orgs" || pathParts[1] == "users" { + path = fmt.Sprintf("%s/%s", pathParts[2], pathParts[4]) + } else { + path = fmt.Sprintf("%s/%s/%s", pathParts[1], pathParts[2], pathParts[4]) + } + paths = append(paths, path) + found = true + break + } + } + if found { + continue + } + for _, p := range projectsV2 { + if strings.EqualFold(projectName, p.Title) { + // format of ResourcePath: /OWNER/REPO/projects/PROJECT_NUMBER or /orgs/ORG/projects/PROJECT_NUMBER or /users/USER/projects/PROJECT_NUBER + // required format of path: OWNER/REPO/PROJECT_NUMBER or ORG/PROJECT_NUMBER or USER/PROJECT_NUMBER + var path string + pathParts := strings.Split(p.ResourcePath, "/") + if pathParts[1] == "orgs" || pathParts[1] == "users" { + path = fmt.Sprintf("%s/%s", pathParts[2], pathParts[4]) + } else { + path = fmt.Sprintf("%s/%s/%s", pathParts[1], pathParts[2], pathParts[4]) + } + paths = append(paths, path) + found = true + break + } + } + if !found { + return nil, fmt.Errorf("'%s' not found", projectName) + } + } + return paths, nil +} + +func (m *RepoMetadataResult) MilestoneToID(title string) (string, error) { + for _, m := range m.Milestones { + if strings.EqualFold(title, m.Title) { + return m.ID, nil + } + } + return "", fmt.Errorf("'%s' not found", title) +} + +func (m *RepoMetadataResult) Merge(m2 *RepoMetadataResult) { + if len(m2.AssignableUsers) > 0 || len(m.AssignableUsers) == 0 { + m.AssignableUsers = m2.AssignableUsers + } + + if len(m2.Teams) > 0 || len(m.Teams) == 0 { + m.Teams = m2.Teams + } + + if len(m2.Labels) > 0 || len(m.Labels) == 0 { + m.Labels = m2.Labels + } + + if len(m2.Projects) > 0 || len(m.Projects) == 0 { + m.Projects = m2.Projects + } + + if len(m2.Milestones) > 0 || len(m.Milestones) == 0 { + m.Milestones = m2.Milestones + } +} + +type RepoMetadataInput struct { + Assignees bool + Reviewers bool + Labels bool + Projects bool + Milestones bool +} + +// RepoMetadata pre-fetches the metadata for attaching to issues and pull requests +func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput) (*RepoMetadataResult, error) { + var result RepoMetadataResult + var g errgroup.Group + + if input.Assignees || input.Reviewers { + g.Go(func() error { + users, err := RepoAssignableUsers(client, repo) + if err != nil { + err = fmt.Errorf("error fetching assignees: %w", err) + } + result.AssignableUsers = users + return err + }) + } + if input.Reviewers { + g.Go(func() error { + teams, err := OrganizationTeams(client, repo) + // TODO: better detection of non-org repos + if err != nil && !strings.Contains(err.Error(), errorResolvingOrganization) { + err = fmt.Errorf("error fetching organization teams: %w", err) + return err + } + result.Teams = teams + return nil + }) + } + if input.Reviewers { + g.Go(func() error { + login, err := CurrentLoginName(client, repo.RepoHost()) + if err != nil { + err = fmt.Errorf("error fetching current login: %w", err) + } + result.CurrentLogin = login + return err + }) + } + if input.Labels { + g.Go(func() error { + labels, err := RepoLabels(client, repo) + if err != nil { + err = fmt.Errorf("error fetching labels: %w", err) + } + result.Labels = labels + return err + }) + } + if input.Projects { + g.Go(func() error { + var err error + result.Projects, result.ProjectsV2, err = relevantProjects(client, repo) + return err + }) + } + if input.Milestones { + g.Go(func() error { + milestones, err := RepoMilestones(client, repo, "open") + if err != nil { + err = fmt.Errorf("error fetching milestones: %w", err) + } + result.Milestones = milestones + return err + }) + } + + if err := g.Wait(); err != nil { + return nil, err + } + + return &result, nil +} + +type RepoResolveInput struct { + Assignees []string + Reviewers []string + Labels []string + Projects []string + Milestones []string +} + +// RepoResolveMetadataIDs looks up GraphQL node IDs in bulk +func RepoResolveMetadataIDs(client *Client, repo ghrepo.Interface, input RepoResolveInput) (*RepoMetadataResult, error) { + users := input.Assignees + hasUser := func(target string) bool { + for _, u := range users { + if strings.EqualFold(u, target) { + return true + } + } + return false + } + + var teams []string + for _, r := range input.Reviewers { + if i := strings.IndexRune(r, '/'); i > -1 { + teams = append(teams, r[i+1:]) + } else if !hasUser(r) { + users = append(users, r) + } + } + + // there is no way to look up projects nor milestones by name, so preload them all + mi := RepoMetadataInput{ + Projects: len(input.Projects) > 0, + Milestones: len(input.Milestones) > 0, + } + result, err := RepoMetadata(client, repo, mi) + if err != nil { + return result, err + } + if len(users) == 0 && len(teams) == 0 && len(input.Labels) == 0 { + return result, nil + } + + query := &bytes.Buffer{} + fmt.Fprint(query, "query RepositoryResolveMetadataIDs {\n") + for i, u := range users { + fmt.Fprintf(query, "u%03d: user(login:%q){id,login}\n", i, u) + } + if len(input.Labels) > 0 { + fmt.Fprintf(query, "repository(owner:%q,name:%q){\n", repo.RepoOwner(), repo.RepoName()) + for i, l := range input.Labels { + fmt.Fprintf(query, "l%03d: label(name:%q){id,name}\n", i, l) + } + fmt.Fprint(query, "}\n") + } + if len(teams) > 0 { + fmt.Fprintf(query, "organization(login:%q){\n", repo.RepoOwner()) + for i, t := range teams { + fmt.Fprintf(query, "t%03d: team(slug:%q){id,slug}\n", i, t) + } + fmt.Fprint(query, "}\n") + } + fmt.Fprint(query, "}\n") + + response := make(map[string]json.RawMessage) + err = client.GraphQL(repo.RepoHost(), query.String(), nil, &response) + if err != nil { + return result, err + } + + for key, v := range response { + switch key { + case "repository": + repoResponse := make(map[string]RepoLabel) + err := json.Unmarshal(v, &repoResponse) + if err != nil { + return result, err + } + for _, l := range repoResponse { + result.Labels = append(result.Labels, l) + } + case "organization": + orgResponse := make(map[string]OrgTeam) + err := json.Unmarshal(v, &orgResponse) + if err != nil { + return result, err + } + for _, t := range orgResponse { + result.Teams = append(result.Teams, t) + } + default: + user := RepoAssignee{} + err := json.Unmarshal(v, &user) + if err != nil { + return result, err + } + result.AssignableUsers = append(result.AssignableUsers, user) + } + } + + return result, nil +} + +type RepoProject struct { + ID string `json:"id"` + Name string `json:"name"` + Number int `json:"number"` + ResourcePath string `json:"resourcePath"` +} + +// RepoProjects fetches all open projects for a repository. +func RepoProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, error) { + type responseData struct { + Repository struct { + Projects struct { + Nodes []RepoProject + PageInfo struct { + HasNextPage bool + EndCursor string + } + } `graphql:"projects(states: [OPEN], first: 100, orderBy: {field: NAME, direction: ASC}, after: $endCursor)"` + } `graphql:"repository(owner: $owner, name: $name)"` + } + + variables := map[string]interface{}{ + "owner": githubv4.String(repo.RepoOwner()), + "name": githubv4.String(repo.RepoName()), + "endCursor": (*githubv4.String)(nil), + } + + var projects []RepoProject + for { + var query responseData + err := client.Query(repo.RepoHost(), "RepositoryProjectList", &query, variables) + if err != nil { + return nil, err + } + + projects = append(projects, query.Repository.Projects.Nodes...) + if !query.Repository.Projects.PageInfo.HasNextPage { + break + } + variables["endCursor"] = githubv4.String(query.Repository.Projects.PageInfo.EndCursor) + } + + return projects, nil +} + +type RepoAssignee struct { + ID string + Login string + Name string +} + +// DisplayName returns a formatted string that uses Login and Name to be displayed e.g. 'Login (Name)' or 'Login' +func (ra RepoAssignee) DisplayName() string { + if ra.Name != "" { + return fmt.Sprintf("%s (%s)", ra.Login, ra.Name) + } + return ra.Login +} + +// RepoAssignableUsers fetches all the assignable users for a repository +func RepoAssignableUsers(client *Client, repo ghrepo.Interface) ([]RepoAssignee, error) { + type responseData struct { + Repository struct { + AssignableUsers struct { + Nodes []RepoAssignee + PageInfo struct { + HasNextPage bool + EndCursor string + } + } `graphql:"assignableUsers(first: 100, after: $endCursor)"` + } `graphql:"repository(owner: $owner, name: $name)"` + } + + variables := map[string]interface{}{ + "owner": githubv4.String(repo.RepoOwner()), + "name": githubv4.String(repo.RepoName()), + "endCursor": (*githubv4.String)(nil), + } + + var users []RepoAssignee + for { + var query responseData + err := client.Query(repo.RepoHost(), "RepositoryAssignableUsers", &query, variables) + if err != nil { + return nil, err + } + + users = append(users, query.Repository.AssignableUsers.Nodes...) + if !query.Repository.AssignableUsers.PageInfo.HasNextPage { + break + } + variables["endCursor"] = githubv4.String(query.Repository.AssignableUsers.PageInfo.EndCursor) + } + + return users, nil +} + +type RepoLabel struct { + ID string + Name string +} + +// RepoLabels fetches all the labels in a repository +func RepoLabels(client *Client, repo ghrepo.Interface) ([]RepoLabel, error) { + type responseData struct { + Repository struct { + Labels struct { + Nodes []RepoLabel + PageInfo struct { + HasNextPage bool + EndCursor string + } + } `graphql:"labels(first: 100, orderBy: {field: NAME, direction: ASC}, after: $endCursor)"` + } `graphql:"repository(owner: $owner, name: $name)"` + } + + variables := map[string]interface{}{ + "owner": githubv4.String(repo.RepoOwner()), + "name": githubv4.String(repo.RepoName()), + "endCursor": (*githubv4.String)(nil), + } + + var labels []RepoLabel + for { + var query responseData + err := client.Query(repo.RepoHost(), "RepositoryLabelList", &query, variables) + if err != nil { + return nil, err + } + + labels = append(labels, query.Repository.Labels.Nodes...) + if !query.Repository.Labels.PageInfo.HasNextPage { + break + } + variables["endCursor"] = githubv4.String(query.Repository.Labels.PageInfo.EndCursor) + } + + return labels, nil +} + +type RepoMilestone struct { + ID string + Title string +} + +// RepoMilestones fetches milestones in a repository +func RepoMilestones(client *Client, repo ghrepo.Interface, state string) ([]RepoMilestone, error) { + type responseData struct { + Repository struct { + Milestones struct { + Nodes []RepoMilestone + PageInfo struct { + HasNextPage bool + EndCursor string + } + } `graphql:"milestones(states: $states, first: 100, after: $endCursor)"` + } `graphql:"repository(owner: $owner, name: $name)"` + } + + var states []githubv4.MilestoneState + switch state { + case "open": + states = []githubv4.MilestoneState{"OPEN"} + case "closed": + states = []githubv4.MilestoneState{"CLOSED"} + case "all": + states = []githubv4.MilestoneState{"OPEN", "CLOSED"} + default: + return nil, fmt.Errorf("invalid state: %s", state) + } + + variables := map[string]interface{}{ + "owner": githubv4.String(repo.RepoOwner()), + "name": githubv4.String(repo.RepoName()), + "states": states, + "endCursor": (*githubv4.String)(nil), + } + + var milestones []RepoMilestone + for { + var query responseData + err := client.Query(repo.RepoHost(), "RepositoryMilestoneList", &query, variables) + if err != nil { + return nil, err + } + + milestones = append(milestones, query.Repository.Milestones.Nodes...) + if !query.Repository.Milestones.PageInfo.HasNextPage { + break + } + variables["endCursor"] = githubv4.String(query.Repository.Milestones.PageInfo.EndCursor) + } + + return milestones, nil +} + +func ProjectNamesToPaths(client *Client, repo ghrepo.Interface, projectNames []string) ([]string, error) { + projects, projectsV2, err := relevantProjects(client, repo) + if err != nil { + return nil, err + } + return ProjectsToPaths(projects, projectsV2, projectNames) +} + +// RelevantProjects retrieves set of Projects and ProjectsV2 relevant to given repository: +// - Projects for repository +// - Projects for repository organization, if it belongs to one +// - ProjectsV2 owned by current user +// - ProjectsV2 linked to repository +// - ProjectsV2 owned by repository organization, if it belongs to one +func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []ProjectV2, error) { + var repoProjects []RepoProject + var orgProjects []RepoProject + var userProjectsV2 []ProjectV2 + var repoProjectsV2 []ProjectV2 + var orgProjectsV2 []ProjectV2 + + g, _ := errgroup.WithContext(context.Background()) + + g.Go(func() error { + var err error + repoProjects, err = RepoProjects(client, repo) + if err != nil { + err = fmt.Errorf("error fetching repo projects (classic): %w", err) + } + return err + }) + g.Go(func() error { + var err error + orgProjects, err = OrganizationProjects(client, repo) + if err != nil && !strings.Contains(err.Error(), errorResolvingOrganization) { + err = fmt.Errorf("error fetching organization projects (classic): %w", err) + return err + } + return nil + }) + g.Go(func() error { + var err error + userProjectsV2, err = CurrentUserProjectsV2(client, repo.RepoHost()) + if err != nil && !ProjectsV2IgnorableError(err) { + err = fmt.Errorf("error fetching user projects: %w", err) + return err + } + return nil + }) + g.Go(func() error { + var err error + repoProjectsV2, err = RepoProjectsV2(client, repo) + if err != nil && !ProjectsV2IgnorableError(err) { + err = fmt.Errorf("error fetching repo projects: %w", err) + return err + } + return nil + }) + g.Go(func() error { + var err error + orgProjectsV2, err = OrganizationProjectsV2(client, repo) + if err != nil && + !ProjectsV2IgnorableError(err) && + !strings.Contains(err.Error(), errorResolvingOrganization) { + err = fmt.Errorf("error fetching organization projects: %w", err) + return err + } + return nil + }) + + if err := g.Wait(); err != nil { + return nil, nil, err + } + + projects := make([]RepoProject, 0, len(repoProjects)+len(orgProjects)) + projects = append(projects, repoProjects...) + projects = append(projects, orgProjects...) + + // ProjectV2 might appear across multiple queries so use a map to keep them deduplicated. + m := make(map[string]ProjectV2, len(userProjectsV2)+len(repoProjectsV2)+len(orgProjectsV2)) + for _, p := range userProjectsV2 { + m[p.ID] = p + } + for _, p := range repoProjectsV2 { + m[p.ID] = p + } + for _, p := range orgProjectsV2 { + m[p.ID] = p + } + projectsV2 := make([]ProjectV2, 0, len(m)) + for _, p := range m { + projectsV2 = append(projectsV2, p) + } + + return projects, projectsV2, nil +} + +func CreateRepoTransformToV4(apiClient *Client, hostname string, method string, path string, body io.Reader) (*Repository, error) { + var responsev3 repositoryV3 + err := apiClient.REST(hostname, method, path, body, &responsev3) + + if err != nil { + return nil, err + } + + return &Repository{ + Name: responsev3.Name, + CreatedAt: responsev3.CreatedAt, + Owner: RepositoryOwner{ + Login: responsev3.Owner.Login, + }, + ID: responsev3.NodeID, + hostname: hostname, + URL: responsev3.HTMLUrl, + IsPrivate: responsev3.Private, + }, nil +} + +// MapReposToIDs retrieves a set of IDs for the given set of repositories. +// This is similar logic to RepoNetwork, but only fetches databaseId and does not +// discover parent repositories. +func GetRepoIDs(client *Client, host string, repositories []ghrepo.Interface) ([]int64, error) { + queries := make([]string, 0, len(repositories)) + for i, repo := range repositories { + queries = append(queries, fmt.Sprintf(` + repo_%03d: repository(owner: %q, name: %q) { + databaseId + } + `, i, repo.RepoOwner(), repo.RepoName())) + } + + query := fmt.Sprintf(`query MapRepositoryNames { %s }`, strings.Join(queries, "")) + + graphqlResult := make(map[string]*struct { + DatabaseID int64 `json:"databaseId"` + }) + + if err := client.GraphQL(host, query, nil, &graphqlResult); err != nil { + return nil, fmt.Errorf("failed to look up repositories: %w", err) + } + + repoKeys := make([]string, 0, len(repositories)) + for k := range graphqlResult { + repoKeys = append(repoKeys, k) + } + sort.Strings(repoKeys) + + result := make([]int64, len(repositories)) + for i, k := range repoKeys { + result[i] = graphqlResult[k].DatabaseID + } + return result, nil +} + +func RepoExists(client *Client, repo ghrepo.Interface) (bool, error) { + path := fmt.Sprintf("%srepos/%s/%s", ghinstance.RESTPrefix(repo.RepoHost()), repo.RepoOwner(), repo.RepoName()) + + resp, err := client.HTTP().Head(path) + if err != nil { + return false, err + } + + switch resp.StatusCode { + case 200: + return true, nil + case 404: + return false, nil + default: + return false, ghAPI.HandleHTTPError(resp) + } +} diff --git a/vendor/github.com/cli/cli/v2/api/queries_user.go b/vendor/github.com/cli/cli/v2/api/queries_user.go new file mode 100644 index 000000000..cf0121a8b --- /dev/null +++ b/vendor/github.com/cli/cli/v2/api/queries_user.go @@ -0,0 +1,45 @@ +package api + +type Organization struct { + Login string +} + +func CurrentLoginName(client *Client, hostname string) (string, error) { + var query struct { + Viewer struct { + Login string + } + } + err := client.Query(hostname, "UserCurrent", &query, nil) + return query.Viewer.Login, err +} + +func CurrentLoginNameAndOrgs(client *Client, hostname string) (string, []string, error) { + var query struct { + Viewer struct { + Login string + Organizations struct { + Nodes []Organization + } `graphql:"organizations(first: 100)"` + } + } + err := client.Query(hostname, "UserCurrent", &query, nil) + if err != nil { + return "", nil, err + } + orgNames := []string{} + for _, org := range query.Viewer.Organizations.Nodes { + orgNames = append(orgNames, org.Login) + } + return query.Viewer.Login, orgNames, err +} + +func CurrentUserID(client *Client, hostname string) (string, error) { + var query struct { + Viewer struct { + ID string + } + } + err := client.Query(hostname, "UserCurrent", &query, nil) + return query.Viewer.ID, err +} diff --git a/vendor/github.com/cli/cli/v2/api/query_builder.go b/vendor/github.com/cli/cli/v2/api/query_builder.go new file mode 100644 index 000000000..9c8aaac17 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/api/query_builder.go @@ -0,0 +1,503 @@ +package api + +import ( + "fmt" + "strings" + + "github.com/cli/cli/v2/pkg/set" +) + +func squeeze(r rune) rune { + switch r { + case '\n', '\t': + return -1 + default: + return r + } +} + +func shortenQuery(q string) string { + return strings.Map(squeeze, q) +} + +var issueComments = shortenQuery(` + comments(first: 100) { + nodes { + id, + author{login,...on User{id,name}}, + authorAssociation, + body, + createdAt, + includesCreatedEdit, + isMinimized, + minimizedReason, + reactionGroups{content,users{totalCount}}, + url, + viewerDidAuthor + }, + pageInfo{hasNextPage,endCursor}, + totalCount + } +`) + +var issueCommentLast = shortenQuery(` + comments(last: 1) { + nodes { + author{login,...on User{id,name}}, + authorAssociation, + body, + createdAt, + includesCreatedEdit, + isMinimized, + minimizedReason, + reactionGroups{content,users{totalCount}} + }, + totalCount + } +`) + +var prReviewRequests = shortenQuery(` + reviewRequests(first: 100) { + nodes { + requestedReviewer { + __typename, + ...on User{login}, + ...on Team{ + organization{login} + name, + slug + } + } + } + } +`) + +var prReviews = shortenQuery(` + reviews(first: 100) { + nodes { + id, + author{login}, + authorAssociation, + submittedAt, + body, + state, + commit{oid}, + reactionGroups{content,users{totalCount}} + } + pageInfo{hasNextPage,endCursor} + totalCount + } +`) + +var prLatestReviews = shortenQuery(` + latestReviews(first: 100) { + nodes { + author{login}, + authorAssociation, + submittedAt, + body, + state + } + } +`) + +var prFiles = shortenQuery(` + files(first: 100) { + nodes { + additions, + deletions, + path + } + } +`) + +var prCommits = shortenQuery(` + commits(first: 100) { + nodes { + commit { + authors(first:100) { + nodes { + name, + email, + user{id,login} + } + }, + messageHeadline, + messageBody, + oid, + committedDate, + authoredDate + } + } + } +`) + +var autoMergeRequest = shortenQuery(` + autoMergeRequest { + authorEmail, + commitBody, + commitHeadline, + mergeMethod, + enabledAt, + enabledBy{login,...on User{id,name}} + } +`) + +func StatusCheckRollupGraphQLWithCountByState() string { + return shortenQuery(` + statusCheckRollup: commits(last: 1) { + nodes { + commit { + statusCheckRollup { + contexts { + checkRunCount, + checkRunCountsByState { + state, + count + }, + statusContextCount, + statusContextCountsByState { + state, + count + } + } + } + } + } + }`) +} + +func StatusCheckRollupGraphQLWithoutCountByState(after string) string { + var afterClause string + if after != "" { + afterClause = ",after:" + after + } + return fmt.Sprintf(shortenQuery(` + statusCheckRollup: commits(last: 1) { + nodes { + commit { + statusCheckRollup { + contexts(first:100%s) { + nodes { + __typename + ...on StatusContext { + context, + state, + targetUrl, + createdAt, + description + }, + ...on CheckRun { + name, + checkSuite{workflowRun{workflow{name}}}, + status, + conclusion, + startedAt, + completedAt, + detailsUrl + } + }, + pageInfo{hasNextPage,endCursor} + } + } + } + } + }`), afterClause) +} + +func RequiredStatusCheckRollupGraphQL(prID, after string, includeEvent bool) string { + var afterClause string + if after != "" { + afterClause = ",after:" + after + } + eventField := "event," + if !includeEvent { + eventField = "" + } + return fmt.Sprintf(shortenQuery(` + statusCheckRollup: commits(last: 1) { + nodes { + commit { + statusCheckRollup { + contexts(first:100%[1]s) { + nodes { + __typename + ...on StatusContext { + context, + state, + targetUrl, + createdAt, + description, + isRequired(pullRequestId: %[2]s) + }, + ...on CheckRun { + name, + checkSuite{workflowRun{%[3]sworkflow{name}}}, + status, + conclusion, + startedAt, + completedAt, + detailsUrl, + isRequired(pullRequestId: %[2]s) + } + }, + pageInfo{hasNextPage,endCursor} + } + } + } + } + }`), afterClause, prID, eventField) +} + +var IssueFields = []string{ + "assignees", + "author", + "body", + "closed", + "comments", + "createdAt", + "closedAt", + "id", + "labels", + "milestone", + "number", + "projectCards", + "projectItems", + "reactionGroups", + "state", + "title", + "updatedAt", + "url", +} + +var PullRequestFields = append(IssueFields, + "additions", + "autoMergeRequest", + "baseRefName", + "changedFiles", + "commits", + "deletions", + "files", + "headRefName", + "headRefOid", + "headRepository", + "headRepositoryOwner", + "isCrossRepository", + "isDraft", + "latestReviews", + "maintainerCanModify", + "mergeable", + "mergeCommit", + "mergedAt", + "mergedBy", + "mergeStateStatus", + "potentialMergeCommit", + "reviewDecision", + "reviewRequests", + "reviews", + "statusCheckRollup", +) + +// IssueGraphQL constructs a GraphQL query fragment for a set of issue fields. +func IssueGraphQL(fields []string) string { + var q []string + for _, field := range fields { + switch field { + case "author": + q = append(q, `author{login,...on User{id,name}}`) + case "mergedBy": + q = append(q, `mergedBy{login,...on User{id,name}}`) + case "headRepositoryOwner": + q = append(q, `headRepositoryOwner{id,login,...on User{name}}`) + case "headRepository": + q = append(q, `headRepository{id,name}`) + case "assignees": + q = append(q, `assignees(first:100){nodes{id,login,name},totalCount}`) + case "labels": + q = append(q, `labels(first:100){nodes{id,name,description,color},totalCount}`) + case "projectCards": + q = append(q, `projectCards(first:100){nodes{project{name}column{name}},totalCount}`) + case "projectItems": + q = append(q, `projectItems(first:100){nodes{id, project{id,title}, status:fieldValueByName(name: "Status") { ... on ProjectV2ItemFieldSingleSelectValue{optionId,name}}},totalCount}`) + case "milestone": + q = append(q, `milestone{number,title,description,dueOn}`) + case "reactionGroups": + q = append(q, `reactionGroups{content,users{totalCount}}`) + case "mergeCommit": + q = append(q, `mergeCommit{oid}`) + case "potentialMergeCommit": + q = append(q, `potentialMergeCommit{oid}`) + case "autoMergeRequest": + q = append(q, autoMergeRequest) + case "comments": + q = append(q, issueComments) + case "lastComment": // pseudo-field + q = append(q, issueCommentLast) + case "reviewRequests": + q = append(q, prReviewRequests) + case "reviews": + q = append(q, prReviews) + case "latestReviews": + q = append(q, prLatestReviews) + case "files": + q = append(q, prFiles) + case "commits": + q = append(q, prCommits) + case "lastCommit": // pseudo-field + q = append(q, `commits(last:1){nodes{commit{oid}}}`) + case "commitsCount": // pseudo-field + q = append(q, `commits{totalCount}`) + case "requiresStrictStatusChecks": // pseudo-field + q = append(q, `baseRef{branchProtectionRule{requiresStrictStatusChecks}}`) + case "statusCheckRollup": + q = append(q, StatusCheckRollupGraphQLWithoutCountByState("")) + case "statusCheckRollupWithCountByState": // pseudo-field + q = append(q, StatusCheckRollupGraphQLWithCountByState()) + default: + q = append(q, field) + } + } + return strings.Join(q, ",") +} + +// PullRequestGraphQL constructs a GraphQL query fragment for a set of pull request fields. +// It will try to sanitize the fields to just those available on pull request. +func PullRequestGraphQL(fields []string) string { + invalidFields := []string{"isPinned", "stateReason"} + s := set.NewStringSet() + s.AddValues(fields) + s.RemoveValues(invalidFields) + return IssueGraphQL(s.ToSlice()) +} + +var RepositoryFields = []string{ + "id", + "name", + "nameWithOwner", + "owner", + "parent", + "templateRepository", + "description", + "homepageUrl", + "openGraphImageUrl", + "usesCustomOpenGraphImage", + "url", + "sshUrl", + "mirrorUrl", + "securityPolicyUrl", + + "createdAt", + "pushedAt", + "updatedAt", + + "isBlankIssuesEnabled", + "isSecurityPolicyEnabled", + "hasIssuesEnabled", + "hasProjectsEnabled", + "hasWikiEnabled", + "hasDiscussionsEnabled", + "mergeCommitAllowed", + "squashMergeAllowed", + "rebaseMergeAllowed", + + "forkCount", + "stargazerCount", + "watchers", + "issues", + "pullRequests", + + "codeOfConduct", + "contactLinks", + "defaultBranchRef", + "deleteBranchOnMerge", + "diskUsage", + "fundingLinks", + "isArchived", + "isEmpty", + "isFork", + "isInOrganization", + "isMirror", + "isPrivate", + "visibility", + "isTemplate", + "isUserConfigurationRepository", + "licenseInfo", + "viewerCanAdminister", + "viewerDefaultCommitEmail", + "viewerDefaultMergeMethod", + "viewerHasStarred", + "viewerPermission", + "viewerPossibleCommitEmails", + "viewerSubscription", + + "repositoryTopics", + "primaryLanguage", + "languages", + "issueTemplates", + "pullRequestTemplates", + "labels", + "milestones", + "latestRelease", + + "assignableUsers", + "mentionableUsers", + "projects", + + // "branchProtectionRules", // too complex to expose + // "collaborators", // does it make sense to expose without affiliation filter? +} + +func RepositoryGraphQL(fields []string) string { + var q []string + for _, field := range fields { + switch field { + case "codeOfConduct": + q = append(q, "codeOfConduct{key,name,url}") + case "contactLinks": + q = append(q, "contactLinks{about,name,url}") + case "fundingLinks": + q = append(q, "fundingLinks{platform,url}") + case "licenseInfo": + q = append(q, "licenseInfo{key,name,nickname}") + case "owner": + q = append(q, "owner{id,login}") + case "parent": + q = append(q, "parent{id,name,owner{id,login}}") + case "templateRepository": + q = append(q, "templateRepository{id,name,owner{id,login}}") + case "repositoryTopics": + q = append(q, "repositoryTopics(first:100){nodes{topic{name}}}") + case "issueTemplates": + q = append(q, "issueTemplates{name,title,body,about}") + case "pullRequestTemplates": + q = append(q, "pullRequestTemplates{body,filename}") + case "labels": + q = append(q, "labels(first:100){nodes{id,color,name,description}}") + case "languages": + q = append(q, "languages(first:100){edges{size,node{name}}}") + case "primaryLanguage": + q = append(q, "primaryLanguage{name}") + case "latestRelease": + q = append(q, "latestRelease{publishedAt,tagName,name,url}") + case "milestones": + q = append(q, "milestones(first:100,states:OPEN){nodes{number,title,description,dueOn}}") + case "assignableUsers": + q = append(q, "assignableUsers(first:100){nodes{id,login,name}}") + case "mentionableUsers": + q = append(q, "mentionableUsers(first:100){nodes{id,login,name}}") + case "projects": + q = append(q, "projects(first:100,states:OPEN){nodes{id,name,number,body,resourcePath}}") + case "watchers": + q = append(q, "watchers{totalCount}") + case "issues": + q = append(q, "issues(states:OPEN){totalCount}") + case "pullRequests": + q = append(q, "pullRequests(states:OPEN){totalCount}") + case "defaultBranchRef": + q = append(q, "defaultBranchRef{name}") + default: + q = append(q, field) + } + } + return strings.Join(q, ",") +} diff --git a/vendor/github.com/cli/cli/v2/api/reaction_groups.go b/vendor/github.com/cli/cli/v2/api/reaction_groups.go new file mode 100644 index 000000000..08ae53040 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/api/reaction_groups.go @@ -0,0 +1,59 @@ +package api + +import ( + "bytes" + "encoding/json" +) + +type ReactionGroups []ReactionGroup + +func (rg ReactionGroups) MarshalJSON() ([]byte, error) { + buf := bytes.Buffer{} + buf.WriteRune('[') + encoder := json.NewEncoder(&buf) + encoder.SetEscapeHTML(false) + + hasPrev := false + for _, g := range rg { + if g.Users.TotalCount == 0 { + continue + } + if hasPrev { + buf.WriteRune(',') + } + if err := encoder.Encode(&g); err != nil { + return nil, err + } + hasPrev = true + } + buf.WriteRune(']') + return buf.Bytes(), nil +} + +type ReactionGroup struct { + Content string `json:"content"` + Users ReactionGroupUsers `json:"users"` +} + +type ReactionGroupUsers struct { + TotalCount int `json:"totalCount"` +} + +func (rg ReactionGroup) Count() int { + return rg.Users.TotalCount +} + +func (rg ReactionGroup) Emoji() string { + return reactionEmoji[rg.Content] +} + +var reactionEmoji = map[string]string{ + "THUMBS_UP": "\U0001f44d", + "THUMBS_DOWN": "\U0001f44e", + "LAUGH": "\U0001f604", + "HOORAY": "\U0001f389", + "CONFUSED": "\U0001f615", + "HEART": "\u2764\ufe0f", + "ROCKET": "\U0001f680", + "EYES": "\U0001f440", +} diff --git a/vendor/github.com/cli/cli/v2/context/context.go b/vendor/github.com/cli/cli/v2/context/context.go new file mode 100644 index 000000000..06ef8ca7d --- /dev/null +++ b/vendor/github.com/cli/cli/v2/context/context.go @@ -0,0 +1,172 @@ +// TODO: rename this package to avoid clash with stdlib +package context + +import ( + "errors" + "fmt" + "slices" + "sort" + + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/iostreams" +) + +// Cap the number of git remotes to look up, since the user might have an +// unusually large number of git remotes. +const defaultRemotesForLookup = 5 + +func ResolveRemotesToRepos(remotes Remotes, client *api.Client, base string) (*ResolvedRemotes, error) { + sort.Stable(remotes) + + result := &ResolvedRemotes{ + remotes: remotes, + apiClient: client, + } + + var baseOverride ghrepo.Interface + if base != "" { + var err error + baseOverride, err = ghrepo.FromFullName(base) + if err != nil { + return result, err + } + result.baseOverride = baseOverride + } + + return result, nil +} + +func resolveNetwork(result *ResolvedRemotes, remotesForLookup int) error { + var repos []ghrepo.Interface + for _, r := range result.remotes { + repos = append(repos, r) + if len(repos) == remotesForLookup { + break + } + } + + networkResult, err := api.RepoNetwork(result.apiClient, repos) + result.network = &networkResult + return err +} + +type ResolvedRemotes struct { + baseOverride ghrepo.Interface + remotes Remotes + network *api.RepoNetworkResult + apiClient *api.Client +} + +func (r *ResolvedRemotes) BaseRepo(io *iostreams.IOStreams) (ghrepo.Interface, error) { + if r.baseOverride != nil { + return r.baseOverride, nil + } + + if len(r.remotes) == 0 { + return nil, errors.New("no git remotes") + } + + // if any of the remotes already has a resolution, respect that + for _, r := range r.remotes { + if r.Resolved == "base" { + return r, nil + } else if r.Resolved != "" { + repo, err := ghrepo.FromFullName(r.Resolved) + if err != nil { + return nil, err + } + return ghrepo.NewWithHost(repo.RepoOwner(), repo.RepoName(), r.RepoHost()), nil + } + } + + if !io.CanPrompt() { + // we cannot prompt, so just resort to the 1st remote + return r.remotes[0], nil + } + + repos, err := r.NetworkRepos(defaultRemotesForLookup) + if err != nil { + return nil, err + } + + if len(repos) == 0 { + return r.remotes[0], nil + } else if len(repos) == 1 { + return repos[0], nil + } + + cs := io.ColorScheme() + + fmt.Fprintf(io.ErrOut, + "%s No default remote repository has been set for this directory.\n", + cs.FailureIcon()) + + fmt.Fprintln(io.Out) + + return nil, errors.New( + "please run `gh repo set-default` to select a default remote repository.") +} + +func (r *ResolvedRemotes) HeadRepos() ([]*api.Repository, error) { + if r.network == nil { + err := resolveNetwork(r, defaultRemotesForLookup) + if err != nil { + return nil, err + } + } + + var results []*api.Repository + var ids []string // Check if repo duplicates + for _, repo := range r.network.Repositories { + if repo != nil && repo.ViewerCanPush() && !slices.Contains(ids, repo.ID) { + results = append(results, repo) + ids = append(ids, repo.ID) + } + } + return results, nil +} + +// NetworkRepos fetches info about remotes for the network of repos. +// Pass a value of 0 to fetch info on all remotes. +func (r *ResolvedRemotes) NetworkRepos(remotesForLookup int) ([]*api.Repository, error) { + if r.network == nil { + err := resolveNetwork(r, remotesForLookup) + if err != nil { + return nil, err + } + } + + var repos []*api.Repository + repoMap := map[string]bool{} + + add := func(r *api.Repository) { + fn := ghrepo.FullName(r) + if _, ok := repoMap[fn]; !ok { + repoMap[fn] = true + repos = append(repos, r) + } + } + + for _, repo := range r.network.Repositories { + if repo == nil { + continue + } + if repo.Parent != nil { + add(repo.Parent) + } + add(repo) + } + + return repos, nil +} + +// RemoteForRepo finds the git remote that points to a repository +func (r *ResolvedRemotes) RemoteForRepo(repo ghrepo.Interface) (*Remote, error) { + for _, remote := range r.remotes { + if ghrepo.IsSame(remote, repo) { + return remote, nil + } + } + return nil, errors.New("not found") +} diff --git a/vendor/github.com/cli/cli/v2/context/remote.go b/vendor/github.com/cli/cli/v2/context/remote.go new file mode 100644 index 000000000..3540ad95d --- /dev/null +++ b/vendor/github.com/cli/cli/v2/context/remote.go @@ -0,0 +1,123 @@ +package context + +import ( + "fmt" + "net/url" + "strings" + + "github.com/cli/cli/v2/git" + "github.com/cli/cli/v2/internal/ghrepo" +) + +// Remotes represents a set of git remotes +type Remotes []*Remote + +// FindByName returns the first Remote whose name matches the list +func (r Remotes) FindByName(names ...string) (*Remote, error) { + for _, name := range names { + for _, rem := range r { + if rem.Name == name || name == "*" { + return rem, nil + } + } + } + return nil, fmt.Errorf("no matching remote found") +} + +// FindByRepo returns the first Remote that points to a specific GitHub repository +func (r Remotes) FindByRepo(owner, name string) (*Remote, error) { + for _, rem := range r { + if strings.EqualFold(rem.RepoOwner(), owner) && strings.EqualFold(rem.RepoName(), name) { + return rem, nil + } + } + return nil, fmt.Errorf("no matching remote found") +} + +// Filter remotes by given hostnames, maintains original order +func (r Remotes) FilterByHosts(hosts []string) Remotes { + filtered := make(Remotes, 0) + for _, rr := range r { + for _, host := range hosts { + if strings.EqualFold(rr.RepoHost(), host) { + filtered = append(filtered, rr) + break + } + } + } + return filtered +} + +func (r Remotes) ResolvedRemote() (*Remote, error) { + for _, rr := range r { + if rr.Resolved != "" { + return rr, nil + } + } + return nil, fmt.Errorf("no resolved remote found") +} + +func remoteNameSortScore(name string) int { + switch strings.ToLower(name) { + case "upstream": + return 3 + case "github": + return 2 + case "origin": + return 1 + default: + return 0 + } +} + +// https://golang.org/pkg/sort/#Interface +func (r Remotes) Len() int { return len(r) } +func (r Remotes) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r Remotes) Less(i, j int) bool { + return remoteNameSortScore(r[i].Name) > remoteNameSortScore(r[j].Name) +} + +// Remote represents a git remote mapped to a GitHub repository +type Remote struct { + *git.Remote + Repo ghrepo.Interface +} + +// RepoName is the name of the GitHub repository +func (r Remote) RepoName() string { + return r.Repo.RepoName() +} + +// RepoOwner is the name of the GitHub account that owns the repo +func (r Remote) RepoOwner() string { + return r.Repo.RepoOwner() +} + +// RepoHost is the GitHub hostname that the remote points to +func (r Remote) RepoHost() string { + return r.Repo.RepoHost() +} + +type Translator interface { + Translate(*url.URL) *url.URL +} + +func TranslateRemotes(gitRemotes git.RemoteSet, translator Translator) (remotes Remotes) { + for _, r := range gitRemotes { + var repo ghrepo.Interface + if r.FetchURL != nil { + repo, _ = ghrepo.FromURL(translator.Translate(r.FetchURL)) + } + if r.PushURL != nil && repo == nil { + repo, _ = ghrepo.FromURL(translator.Translate(r.PushURL)) + } + if repo == nil { + continue + } + remotes = append(remotes, &Remote{ + Remote: r, + Repo: repo, + }) + } + return +} diff --git a/vendor/github.com/cli/cli/v2/git/client.go b/vendor/github.com/cli/cli/v2/git/client.go new file mode 100644 index 000000000..b0194affc --- /dev/null +++ b/vendor/github.com/cli/cli/v2/git/client.go @@ -0,0 +1,726 @@ +package git + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "net/url" + "os/exec" + "path" + "regexp" + "runtime" + "sort" + "strings" + "sync" + + "github.com/cli/safeexec" +) + +var remoteRE = regexp.MustCompile(`(.+)\s+(.+)\s+\((push|fetch)\)`) + +// This regexp exists to match lines of the following form: +// 6a6872b918c601a0e730710ad8473938a7516d30\u0000title 1\u0000Body 1\u0000\n +// 7a6872b918c601a0e730710ad8473938a7516d31\u0000title 2\u0000Body 2\u0000 +// +// This is the format we use when collecting commit information, +// with null bytes as separators. Using null bytes this way allows for us +// to easily maintain newlines that might be in the body. +// +// The ?m modifier is the multi-line modifier, meaning that ^ and $ +// match the beginning and end of lines, respectively. +// +// The [\S\s] matches any whitespace or non-whitespace character, +// which is different from .* because it allows for newlines as well. +// +// The ? following .* and [\S\s] is a lazy modifier, meaning that it will +// match as few characters as possible while still satisfying the rest of the regexp. +// This is important because it allows us to match the first null byte after the title and body, +// rather than the last null byte in the entire string. +var commitLogRE = regexp.MustCompile(`(?m)^[0-9a-fA-F]{7,40}\x00.*?\x00[\S\s]*?\x00$`) + +type errWithExitCode interface { + ExitCode() int +} + +type Client struct { + GhPath string + RepoDir string + GitPath string + Stderr io.Writer + Stdin io.Reader + Stdout io.Writer + + commandContext commandCtx + mu sync.Mutex +} + +func (c *Client) Copy() *Client { + return &Client{ + GhPath: c.GhPath, + RepoDir: c.RepoDir, + GitPath: c.GitPath, + Stderr: c.Stderr, + Stdin: c.Stdin, + Stdout: c.Stdout, + + commandContext: c.commandContext, + } +} + +func (c *Client) Command(ctx context.Context, args ...string) (*Command, error) { + if c.RepoDir != "" { + args = append([]string{"-C", c.RepoDir}, args...) + } + commandContext := exec.CommandContext + if c.commandContext != nil { + commandContext = c.commandContext + } + var err error + c.mu.Lock() + if c.GitPath == "" { + c.GitPath, err = resolveGitPath() + } + c.mu.Unlock() + if err != nil { + return nil, err + } + cmd := commandContext(ctx, c.GitPath, args...) + cmd.Stderr = c.Stderr + cmd.Stdin = c.Stdin + cmd.Stdout = c.Stdout + return &Command{cmd}, nil +} + +// AuthenticatedCommand is a wrapper around Command that included configuration to use gh +// as the credential helper for git. +func (c *Client) AuthenticatedCommand(ctx context.Context, args ...string) (*Command, error) { + preArgs := []string{"-c", "credential.helper="} + if c.GhPath == "" { + // Assumes that gh is in PATH. + c.GhPath = "gh" + } + credHelper := fmt.Sprintf("!%q auth git-credential", c.GhPath) + preArgs = append(preArgs, "-c", fmt.Sprintf("credential.helper=%s", credHelper)) + args = append(preArgs, args...) + return c.Command(ctx, args...) +} + +func (c *Client) Remotes(ctx context.Context) (RemoteSet, error) { + remoteArgs := []string{"remote", "-v"} + remoteCmd, err := c.Command(ctx, remoteArgs...) + if err != nil { + return nil, err + } + remoteOut, remoteErr := remoteCmd.Output() + if remoteErr != nil { + return nil, remoteErr + } + + configArgs := []string{"config", "--get-regexp", `^remote\..*\.gh-resolved$`} + configCmd, err := c.Command(ctx, configArgs...) + if err != nil { + return nil, err + } + configOut, configErr := configCmd.Output() + if configErr != nil { + // Ignore exit code 1 as it means there are no resolved remotes. + var gitErr *GitError + if ok := errors.As(configErr, &gitErr); ok && gitErr.ExitCode != 1 { + return nil, gitErr + } + } + + remotes := parseRemotes(outputLines(remoteOut)) + populateResolvedRemotes(remotes, outputLines(configOut)) + sort.Sort(remotes) + return remotes, nil +} + +func (c *Client) UpdateRemoteURL(ctx context.Context, name, url string) error { + args := []string{"remote", "set-url", name, url} + cmd, err := c.Command(ctx, args...) + if err != nil { + return err + } + _, err = cmd.Output() + if err != nil { + return err + } + return nil +} + +func (c *Client) SetRemoteResolution(ctx context.Context, name, resolution string) error { + args := []string{"config", "--add", fmt.Sprintf("remote.%s.gh-resolved", name), resolution} + cmd, err := c.Command(ctx, args...) + if err != nil { + return err + } + _, err = cmd.Output() + if err != nil { + return err + } + return nil +} + +// CurrentBranch reads the checked-out branch for the git repository. +func (c *Client) CurrentBranch(ctx context.Context) (string, error) { + args := []string{"symbolic-ref", "--quiet", "HEAD"} + cmd, err := c.Command(ctx, args...) + if err != nil { + return "", err + } + out, err := cmd.Output() + if err != nil { + var gitErr *GitError + if ok := errors.As(err, &gitErr); ok && len(gitErr.Stderr) == 0 { + gitErr.err = ErrNotOnAnyBranch + gitErr.Stderr = "not on any branch" + return "", gitErr + } + return "", err + } + branch := firstLine(out) + return strings.TrimPrefix(branch, "refs/heads/"), nil +} + +// ShowRefs resolves fully-qualified refs to commit hashes. +func (c *Client) ShowRefs(ctx context.Context, refs []string) ([]Ref, error) { + args := append([]string{"show-ref", "--verify", "--"}, refs...) + cmd, err := c.Command(ctx, args...) + if err != nil { + return nil, err + } + // This functionality relies on parsing output from the git command despite + // an error status being returned from git. + out, err := cmd.Output() + var verified []Ref + for _, line := range outputLines(out) { + parts := strings.SplitN(line, " ", 2) + if len(parts) < 2 { + continue + } + verified = append(verified, Ref{ + Hash: parts[0], + Name: parts[1], + }) + } + return verified, err +} + +func (c *Client) Config(ctx context.Context, name string) (string, error) { + args := []string{"config", name} + cmd, err := c.Command(ctx, args...) + if err != nil { + return "", err + } + out, err := cmd.Output() + if err != nil { + var gitErr *GitError + if ok := errors.As(err, &gitErr); ok && gitErr.ExitCode == 1 { + gitErr.Stderr = fmt.Sprintf("unknown config key %s", name) + return "", gitErr + } + return "", err + } + return firstLine(out), nil +} + +func (c *Client) UncommittedChangeCount(ctx context.Context) (int, error) { + args := []string{"status", "--porcelain"} + cmd, err := c.Command(ctx, args...) + if err != nil { + return 0, err + } + out, err := cmd.Output() + if err != nil { + return 0, err + } + lines := strings.Split(string(out), "\n") + count := 0 + for _, l := range lines { + if l != "" { + count++ + } + } + return count, nil +} + +func (c *Client) Commits(ctx context.Context, baseRef, headRef string) ([]*Commit, error) { + // The formatting directive %x00 indicates that git should include the null byte as a separator. + // We use this because it is not a valid character to include in a commit message. Previously, + // commas were used here but when we Split on them, we would get incorrect results if commit titles + // happened to contain them. + // https://git-scm.com/docs/pretty-formats#Documentation/pretty-formats.txt-emx00em + args := []string{"-c", "log.ShowSignature=false", "log", "--pretty=format:%H%x00%s%x00%b%x00", "--cherry", fmt.Sprintf("%s...%s", baseRef, headRef)} + cmd, err := c.Command(ctx, args...) + if err != nil { + return nil, err + } + out, err := cmd.Output() + if err != nil { + return nil, err + } + + commits := []*Commit{} + commitLogs := commitLogRE.FindAllString(string(out), -1) + for _, commitLog := range commitLogs { + // Each line looks like this: + // 6a6872b918c601a0e730710ad8473938a7516d30\u0000title 1\u0000Body 1\u0000\n + + // Or with an optional body: + // 6a6872b918c601a0e730710ad8473938a7516d30\u0000title 1\u0000\u0000\n + + // Therefore after splitting we will have: + // ["6a6872b918c601a0e730710ad8473938a7516d30", "title 1", "Body 1", ""] + + // Or with an optional body: + // ["6a6872b918c601a0e730710ad8473938a7516d30", "title 1", "", ""] + commitLogParts := strings.Split(commitLog, "\u0000") + commits = append(commits, &Commit{ + Sha: commitLogParts[0], + Title: commitLogParts[1], + Body: commitLogParts[2], + }) + } + + if len(commits) == 0 { + return nil, fmt.Errorf("could not find any commits between %s and %s", baseRef, headRef) + } + + return commits, nil +} + +func (c *Client) LastCommit(ctx context.Context) (*Commit, error) { + output, err := c.lookupCommit(ctx, "HEAD", "%H,%s") + if err != nil { + return nil, err + } + idx := bytes.IndexByte(output, ',') + return &Commit{ + Sha: string(output[0:idx]), + Title: strings.TrimSpace(string(output[idx+1:])), + }, nil +} + +func (c *Client) CommitBody(ctx context.Context, sha string) (string, error) { + output, err := c.lookupCommit(ctx, sha, "%b") + return string(output), err +} + +func (c *Client) lookupCommit(ctx context.Context, sha, format string) ([]byte, error) { + args := []string{"-c", "log.ShowSignature=false", "show", "-s", "--pretty=format:" + format, sha} + cmd, err := c.Command(ctx, args...) + if err != nil { + return nil, err + } + out, err := cmd.Output() + if err != nil { + return nil, err + } + return out, nil +} + +// ReadBranchConfig parses the `branch.BRANCH.(remote|merge)` part of git config. +func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (cfg BranchConfig) { + prefix := regexp.QuoteMeta(fmt.Sprintf("branch.%s.", branch)) + args := []string{"config", "--get-regexp", fmt.Sprintf("^%s(remote|merge)$", prefix)} + cmd, err := c.Command(ctx, args...) + if err != nil { + return + } + out, err := cmd.Output() + if err != nil { + return + } + for _, line := range outputLines(out) { + parts := strings.SplitN(line, " ", 2) + if len(parts) < 2 { + continue + } + keys := strings.Split(parts[0], ".") + switch keys[len(keys)-1] { + case "remote": + if strings.Contains(parts[1], ":") { + u, err := ParseURL(parts[1]) + if err != nil { + continue + } + cfg.RemoteURL = u + } else if !isFilesystemPath(parts[1]) { + cfg.RemoteName = parts[1] + } + case "merge": + cfg.MergeRef = parts[1] + } + } + return +} + +func (c *Client) DeleteLocalTag(ctx context.Context, tag string) error { + args := []string{"tag", "-d", tag} + cmd, err := c.Command(ctx, args...) + if err != nil { + return err + } + _, err = cmd.Output() + if err != nil { + return err + } + return nil +} + +func (c *Client) DeleteLocalBranch(ctx context.Context, branch string) error { + args := []string{"branch", "-D", branch} + cmd, err := c.Command(ctx, args...) + if err != nil { + return err + } + _, err = cmd.Output() + if err != nil { + return err + } + return nil +} + +func (c *Client) CheckoutBranch(ctx context.Context, branch string) error { + args := []string{"checkout", branch} + cmd, err := c.Command(ctx, args...) + if err != nil { + return err + } + _, err = cmd.Output() + if err != nil { + return err + } + return nil +} + +func (c *Client) CheckoutNewBranch(ctx context.Context, remoteName, branch string) error { + track := fmt.Sprintf("%s/%s", remoteName, branch) + args := []string{"checkout", "-b", branch, "--track", track} + cmd, err := c.Command(ctx, args...) + if err != nil { + return err + } + _, err = cmd.Output() + if err != nil { + return err + } + return nil +} + +func (c *Client) HasLocalBranch(ctx context.Context, branch string) bool { + _, err := c.revParse(ctx, "--verify", "refs/heads/"+branch) + return err == nil +} + +func (c *Client) TrackingBranchNames(ctx context.Context, prefix string) []string { + args := []string{"branch", "-r", "--format", "%(refname:strip=3)"} + if prefix != "" { + args = append(args, "--list", fmt.Sprintf("*/%s*", escapeGlob(prefix))) + } + cmd, err := c.Command(ctx, args...) + if err != nil { + return nil + } + output, err := cmd.Output() + if err != nil { + return nil + } + return strings.Split(string(output), "\n") +} + +// ToplevelDir returns the top-level directory path of the current repository. +func (c *Client) ToplevelDir(ctx context.Context) (string, error) { + out, err := c.revParse(ctx, "--show-toplevel") + if err != nil { + return "", err + } + return firstLine(out), nil +} + +func (c *Client) GitDir(ctx context.Context) (string, error) { + out, err := c.revParse(ctx, "--git-dir") + if err != nil { + return "", err + } + return firstLine(out), nil +} + +// Show current directory relative to the top-level directory of repository. +func (c *Client) PathFromRoot(ctx context.Context) string { + out, err := c.revParse(ctx, "--show-prefix") + if err != nil { + return "" + } + if path := firstLine(out); path != "" { + return path[:len(path)-1] + } + return "" +} + +func (c *Client) revParse(ctx context.Context, args ...string) ([]byte, error) { + args = append([]string{"rev-parse"}, args...) + cmd, err := c.Command(ctx, args...) + if err != nil { + return nil, err + } + return cmd.Output() +} + +func (c *Client) IsLocalGitRepo(ctx context.Context) (bool, error) { + _, err := c.GitDir(ctx) + if err != nil { + var execError errWithExitCode + if errors.As(err, &execError) && execError.ExitCode() == 128 { + return false, nil + } + return false, err + } + return true, nil +} + +func (c *Client) UnsetRemoteResolution(ctx context.Context, name string) error { + args := []string{"config", "--unset", fmt.Sprintf("remote.%s.gh-resolved", name)} + cmd, err := c.Command(ctx, args...) + if err != nil { + return err + } + _, err = cmd.Output() + if err != nil { + return err + } + return nil +} + +func (c *Client) SetRemoteBranches(ctx context.Context, remote string, refspec string) error { + args := []string{"remote", "set-branches", remote, refspec} + cmd, err := c.Command(ctx, args...) + if err != nil { + return err + } + _, err = cmd.Output() + if err != nil { + return err + } + return nil +} + +func (c *Client) AddRemote(ctx context.Context, name, urlStr string, trackingBranches []string) (*Remote, error) { + args := []string{"remote", "add"} + for _, branch := range trackingBranches { + args = append(args, "-t", branch) + } + args = append(args, name, urlStr) + cmd, err := c.Command(ctx, args...) + if err != nil { + return nil, err + } + if _, err := cmd.Output(); err != nil { + return nil, err + } + var urlParsed *url.URL + if strings.HasPrefix(urlStr, "https") { + urlParsed, err = url.Parse(urlStr) + if err != nil { + return nil, err + } + } else { + urlParsed, err = ParseURL(urlStr) + if err != nil { + return nil, err + } + } + remote := &Remote{ + Name: name, + FetchURL: urlParsed, + PushURL: urlParsed, + } + return remote, nil +} + +// Below are commands that make network calls and need authentication credentials supplied from gh. + +func (c *Client) Fetch(ctx context.Context, remote string, refspec string, mods ...CommandModifier) error { + args := []string{"fetch", remote} + if refspec != "" { + args = append(args, refspec) + } + cmd, err := c.AuthenticatedCommand(ctx, args...) + if err != nil { + return err + } + for _, mod := range mods { + mod(cmd) + } + return cmd.Run() +} + +func (c *Client) Pull(ctx context.Context, remote, branch string, mods ...CommandModifier) error { + args := []string{"pull", "--ff-only"} + if remote != "" && branch != "" { + args = append(args, remote, branch) + } + cmd, err := c.AuthenticatedCommand(ctx, args...) + if err != nil { + return err + } + for _, mod := range mods { + mod(cmd) + } + return cmd.Run() +} + +func (c *Client) Push(ctx context.Context, remote string, ref string, mods ...CommandModifier) error { + args := []string{"push", "--set-upstream", remote, ref} + cmd, err := c.AuthenticatedCommand(ctx, args...) + if err != nil { + return err + } + for _, mod := range mods { + mod(cmd) + } + return cmd.Run() +} + +func (c *Client) Clone(ctx context.Context, cloneURL string, args []string, mods ...CommandModifier) (string, error) { + cloneArgs, target := parseCloneArgs(args) + cloneArgs = append(cloneArgs, cloneURL) + // If the args contain an explicit target, pass it to clone otherwise, + // parse the URL to determine where git cloned it to so we can return it. + if target != "" { + cloneArgs = append(cloneArgs, target) + } else { + target = path.Base(strings.TrimSuffix(cloneURL, ".git")) + } + cloneArgs = append([]string{"clone"}, cloneArgs...) + cmd, err := c.AuthenticatedCommand(ctx, cloneArgs...) + if err != nil { + return "", err + } + for _, mod := range mods { + mod(cmd) + } + err = cmd.Run() + if err != nil { + return "", err + } + return target, nil +} + +func resolveGitPath() (string, error) { + path, err := safeexec.LookPath("git") + if err != nil { + if errors.Is(err, exec.ErrNotFound) { + programName := "git" + if runtime.GOOS == "windows" { + programName = "Git for Windows" + } + return "", &NotInstalled{ + message: fmt.Sprintf("unable to find git executable in PATH; please install %s before retrying", programName), + err: err, + } + } + return "", err + } + return path, nil +} + +func isFilesystemPath(p string) bool { + return p == "." || strings.HasPrefix(p, "./") || strings.HasPrefix(p, "/") +} + +func outputLines(output []byte) []string { + lines := strings.TrimSuffix(string(output), "\n") + return strings.Split(lines, "\n") +} + +func firstLine(output []byte) string { + if i := bytes.IndexAny(output, "\n"); i >= 0 { + return string(output)[0:i] + } + return string(output) +} + +func parseCloneArgs(extraArgs []string) (args []string, target string) { + args = extraArgs + if len(args) > 0 { + if !strings.HasPrefix(args[0], "-") { + target, args = args[0], args[1:] + } + } + return +} + +func parseRemotes(remotesStr []string) RemoteSet { + remotes := RemoteSet{} + for _, r := range remotesStr { + match := remoteRE.FindStringSubmatch(r) + if match == nil { + continue + } + name := strings.TrimSpace(match[1]) + urlStr := strings.TrimSpace(match[2]) + urlType := strings.TrimSpace(match[3]) + + url, err := ParseURL(urlStr) + if err != nil { + continue + } + + var rem *Remote + if len(remotes) > 0 { + rem = remotes[len(remotes)-1] + if name != rem.Name { + rem = nil + } + } + if rem == nil { + rem = &Remote{Name: name} + remotes = append(remotes, rem) + } + + switch urlType { + case "fetch": + rem.FetchURL = url + case "push": + rem.PushURL = url + } + } + return remotes +} + +func populateResolvedRemotes(remotes RemoteSet, resolved []string) { + for _, l := range resolved { + parts := strings.SplitN(l, " ", 2) + if len(parts) < 2 { + continue + } + rp := strings.SplitN(parts[0], ".", 3) + if len(rp) < 2 { + continue + } + name := rp[1] + for _, r := range remotes { + if r.Name == name { + r.Resolved = parts[1] + break + } + } + } +} + +var globReplacer = strings.NewReplacer( + "*", `\*`, + "?", `\?`, + "[", `\[`, + "]", `\]`, + "{", `\{`, + "}", `\}`, +) + +func escapeGlob(p string) string { + return globReplacer.Replace(p) +} diff --git a/vendor/github.com/cli/cli/v2/git/command.go b/vendor/github.com/cli/cli/v2/git/command.go new file mode 100644 index 000000000..8065ffd86 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/git/command.go @@ -0,0 +1,100 @@ +package git + +import ( + "bytes" + "context" + "errors" + "io" + "os/exec" + + "github.com/cli/cli/v2/internal/run" +) + +type commandCtx = func(ctx context.Context, name string, args ...string) *exec.Cmd + +type Command struct { + *exec.Cmd +} + +func (gc *Command) Run() error { + stderr := &bytes.Buffer{} + if gc.Cmd.Stderr == nil { + gc.Cmd.Stderr = stderr + } + // This is a hack in order to not break the hundreds of + // existing tests that rely on `run.PrepareCmd` to be invoked. + err := run.PrepareCmd(gc.Cmd).Run() + if err != nil { + ge := GitError{err: err, Stderr: stderr.String()} + var exitError *exec.ExitError + if errors.As(err, &exitError) { + ge.ExitCode = exitError.ExitCode() + } + return &ge + } + return nil +} + +func (gc *Command) Output() ([]byte, error) { + gc.Stdout = nil + gc.Stderr = nil + // This is a hack in order to not break the hundreds of + // existing tests that rely on `run.PrepareCmd` to be invoked. + out, err := run.PrepareCmd(gc.Cmd).Output() + if err != nil { + ge := GitError{err: err} + var exitError *exec.ExitError + if errors.As(err, &exitError) { + ge.Stderr = string(exitError.Stderr) + ge.ExitCode = exitError.ExitCode() + } + err = &ge + } + return out, err +} + +func (gc *Command) setRepoDir(repoDir string) { + for i, arg := range gc.Args { + if arg == "-C" { + gc.Args[i+1] = repoDir + return + } + } + // Handle "--" invocations for testing purposes. + var index int + for i, arg := range gc.Args { + if arg == "--" { + index = i + 1 + } + } + gc.Args = append(gc.Args[:index+3], gc.Args[index+1:]...) + gc.Args[index+1] = "-C" + gc.Args[index+2] = repoDir +} + +// Allow individual commands to be modified from the default client options. +type CommandModifier func(*Command) + +func WithStderr(stderr io.Writer) CommandModifier { + return func(gc *Command) { + gc.Stderr = stderr + } +} + +func WithStdout(stdout io.Writer) CommandModifier { + return func(gc *Command) { + gc.Stdout = stdout + } +} + +func WithStdin(stdin io.Reader) CommandModifier { + return func(gc *Command) { + gc.Stdin = stdin + } +} + +func WithRepoDir(repoDir string) CommandModifier { + return func(gc *Command) { + gc.setRepoDir(repoDir) + } +} diff --git a/vendor/github.com/cli/cli/v2/git/errors.go b/vendor/github.com/cli/cli/v2/git/errors.go new file mode 100644 index 000000000..a3f1645aa --- /dev/null +++ b/vendor/github.com/cli/cli/v2/git/errors.go @@ -0,0 +1,39 @@ +package git + +import ( + "errors" + "fmt" +) + +// ErrNotOnAnyBranch indicates that the user is in detached HEAD state. +var ErrNotOnAnyBranch = errors.New("git: not on any branch") + +type NotInstalled struct { + message string + err error +} + +func (e *NotInstalled) Error() string { + return e.message +} + +func (e *NotInstalled) Unwrap() error { + return e.err +} + +type GitError struct { + ExitCode int + Stderr string + err error +} + +func (ge *GitError) Error() string { + if ge.Stderr == "" { + return fmt.Sprintf("failed to run git: %v", ge.err) + } + return fmt.Sprintf("failed to run git: %s", ge.Stderr) +} + +func (ge *GitError) Unwrap() error { + return ge.err +} diff --git a/vendor/github.com/cli/cli/v2/git/objects.go b/vendor/github.com/cli/cli/v2/git/objects.go new file mode 100644 index 000000000..c33d92b7c --- /dev/null +++ b/vendor/github.com/cli/cli/v2/git/objects.go @@ -0,0 +1,77 @@ +package git + +import ( + "net/url" + "strings" +) + +// RemoteSet is a slice of git remotes. +type RemoteSet []*Remote + +func (r RemoteSet) Len() int { return len(r) } +func (r RemoteSet) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r RemoteSet) Less(i, j int) bool { + return remoteNameSortScore(r[i].Name) > remoteNameSortScore(r[j].Name) +} + +func remoteNameSortScore(name string) int { + switch strings.ToLower(name) { + case "upstream": + return 3 + case "github": + return 2 + case "origin": + return 1 + default: + return 0 + } +} + +// Remote is a parsed git remote. +type Remote struct { + Name string + Resolved string + FetchURL *url.URL + PushURL *url.URL +} + +func (r *Remote) String() string { + return r.Name +} + +func NewRemote(name string, u string) *Remote { + pu, _ := url.Parse(u) + return &Remote{ + Name: name, + FetchURL: pu, + PushURL: pu, + } +} + +// Ref represents a git commit reference. +type Ref struct { + Hash string + Name string +} + +// TrackingRef represents a ref for a remote tracking branch. +type TrackingRef struct { + RemoteName string + BranchName string +} + +func (r TrackingRef) String() string { + return "refs/remotes/" + r.RemoteName + "/" + r.BranchName +} + +type Commit struct { + Sha string + Title string + Body string +} + +type BranchConfig struct { + RemoteName string + RemoteURL *url.URL + MergeRef string +} diff --git a/vendor/github.com/cli/cli/v2/git/url.go b/vendor/github.com/cli/cli/v2/git/url.go new file mode 100644 index 000000000..1a3e97fd6 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/git/url.go @@ -0,0 +1,64 @@ +package git + +import ( + "net/url" + "strings" +) + +func IsURL(u string) bool { + return strings.HasPrefix(u, "git@") || isSupportedProtocol(u) +} + +func isSupportedProtocol(u string) bool { + return strings.HasPrefix(u, "ssh:") || + strings.HasPrefix(u, "git+ssh:") || + strings.HasPrefix(u, "git:") || + strings.HasPrefix(u, "http:") || + strings.HasPrefix(u, "git+https:") || + strings.HasPrefix(u, "https:") +} + +func isPossibleProtocol(u string) bool { + return isSupportedProtocol(u) || + strings.HasPrefix(u, "ftp:") || + strings.HasPrefix(u, "ftps:") || + strings.HasPrefix(u, "file:") +} + +// ParseURL normalizes git remote urls +func ParseURL(rawURL string) (u *url.URL, err error) { + if !isPossibleProtocol(rawURL) && + strings.ContainsRune(rawURL, ':') && + // not a Windows path + !strings.ContainsRune(rawURL, '\\') { + // support scp-like syntax for ssh protocol + rawURL = "ssh://" + strings.Replace(rawURL, ":", "/", 1) + } + + u, err = url.Parse(rawURL) + if err != nil { + return + } + + if u.Scheme == "git+ssh" { + u.Scheme = "ssh" + } + + if u.Scheme == "git+https" { + u.Scheme = "https" + } + + if u.Scheme != "ssh" { + return + } + + if strings.HasPrefix(u.Path, "//") { + u.Path = strings.TrimPrefix(u.Path, "/") + } + + if idx := strings.Index(u.Host, ":"); idx >= 0 { + u.Host = u.Host[0:idx] + } + + return +} diff --git a/vendor/github.com/cli/cli/v2/internal/authflow/flow.go b/vendor/github.com/cli/cli/v2/internal/authflow/flow.go new file mode 100644 index 000000000..370e08784 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/internal/authflow/flow.go @@ -0,0 +1,157 @@ +package authflow + +import ( + "bufio" + "fmt" + "io" + "net/http" + "net/url" + "regexp" + "strings" + + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/browser" + "github.com/cli/cli/v2/internal/ghinstance" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/utils" + "github.com/cli/oauth" + "github.com/henvic/httpretty" +) + +var ( + // The "GitHub CLI" OAuth app + oauthClientID = "178c6fc778ccc68e1d6a" + // This value is safe to be embedded in version control + oauthClientSecret = "34ddeff2b558a23d38fba8a6de74f086ede1cc0b" + + jsonTypeRE = regexp.MustCompile(`[/+]json($|;)`) +) + +func AuthFlow(oauthHost string, IO *iostreams.IOStreams, notice string, additionalScopes []string, isInteractive bool, b browser.Browser) (string, string, error) { + w := IO.ErrOut + cs := IO.ColorScheme() + + httpClient := &http.Client{} + debugEnabled, debugValue := utils.IsDebugEnabled() + if debugEnabled { + logTraffic := strings.Contains(debugValue, "api") + httpClient.Transport = verboseLog(IO.ErrOut, logTraffic, IO.ColorEnabled())(httpClient.Transport) + } + + minimumScopes := []string{"repo", "read:org", "gist"} + scopes := append(minimumScopes, additionalScopes...) + + callbackURI := "http://127.0.0.1/callback" + if ghinstance.IsEnterprise(oauthHost) { + // the OAuth app on Enterprise hosts is still registered with a legacy callback URL + // see https://github.com/cli/cli/pull/222, https://github.com/cli/cli/pull/650 + callbackURI = "http://localhost/" + } + + flow := &oauth.Flow{ + Host: oauth.GitHubHost(ghinstance.HostPrefix(oauthHost)), + ClientID: oauthClientID, + ClientSecret: oauthClientSecret, + CallbackURI: callbackURI, + Scopes: scopes, + DisplayCode: func(code, verificationURL string) error { + fmt.Fprintf(w, "%s First copy your one-time code: %s\n", cs.Yellow("!"), cs.Bold(code)) + return nil + }, + BrowseURL: func(authURL string) error { + if u, err := url.Parse(authURL); err == nil { + if u.Scheme != "http" && u.Scheme != "https" { + return fmt.Errorf("invalid URL: %s", authURL) + } + } else { + return err + } + + if !isInteractive { + fmt.Fprintf(w, "%s to continue in your web browser: %s\n", cs.Bold("Open this URL"), authURL) + return nil + } + + fmt.Fprintf(w, "%s to open %s in your browser... ", cs.Bold("Press Enter"), oauthHost) + _ = waitForEnter(IO.In) + + if err := b.Browse(authURL); err != nil { + fmt.Fprintf(w, "%s Failed opening a web browser at %s\n", cs.Red("!"), authURL) + fmt.Fprintf(w, " %s\n", err) + fmt.Fprint(w, " Please try entering the URL in your browser manually\n") + } + return nil + }, + WriteSuccessHTML: func(w io.Writer) { + fmt.Fprint(w, oauthSuccessPage) + }, + HTTPClient: httpClient, + Stdin: IO.In, + Stdout: w, + } + + fmt.Fprintln(w, notice) + + token, err := flow.DetectFlow() + if err != nil { + return "", "", err + } + + userLogin, err := getViewer(oauthHost, token.Token, IO.ErrOut) + if err != nil { + return "", "", err + } + + return token.Token, userLogin, nil +} + +type cfg struct { + token string +} + +func (c cfg) ActiveToken(hostname string) (string, string) { + return c.token, "oauth_token" +} + +func getViewer(hostname, token string, logWriter io.Writer) (string, error) { + opts := api.HTTPClientOptions{ + Config: cfg{token: token}, + Log: logWriter, + } + client, err := api.NewHTTPClient(opts) + if err != nil { + return "", err + } + return api.CurrentLoginName(api.NewClientFromHTTP(client), hostname) +} + +func waitForEnter(r io.Reader) error { + scanner := bufio.NewScanner(r) + scanner.Scan() + return scanner.Err() +} + +func verboseLog(out io.Writer, logTraffic bool, colorize bool) func(http.RoundTripper) http.RoundTripper { + logger := &httpretty.Logger{ + Time: true, + TLS: false, + Colors: colorize, + RequestHeader: logTraffic, + RequestBody: logTraffic, + ResponseHeader: logTraffic, + ResponseBody: logTraffic, + Formatters: []httpretty.Formatter{&httpretty.JSONFormatter{}}, + MaxResponseBody: 10000, + } + logger.SetOutput(out) + logger.SetBodyFilter(func(h http.Header) (skip bool, err error) { + return !inspectableMIMEType(h.Get("Content-Type")), nil + }) + return logger.RoundTripper +} + +func inspectableMIMEType(t string) bool { + return strings.HasPrefix(t, "text/") || + strings.HasPrefix(t, "application/x-www-form-urlencoded") || + jsonTypeRE.MatchString(t) +} diff --git a/vendor/github.com/cli/cli/v2/internal/authflow/success.go b/vendor/github.com/cli/cli/v2/internal/authflow/success.go new file mode 100644 index 000000000..a546e58d7 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/internal/authflow/success.go @@ -0,0 +1,42 @@ +package authflow + +const oauthSuccessPage = ` + + +Success: GitHub CLI + + + +
+

Successfully authenticated GitHub CLI

+

You may now close this tab and return to the terminal.

+
+ +` diff --git a/vendor/github.com/cli/cli/v2/internal/browser/browser.go b/vendor/github.com/cli/cli/v2/internal/browser/browser.go new file mode 100644 index 000000000..0f231ddc8 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/internal/browser/browser.go @@ -0,0 +1,16 @@ +package browser + +import ( + "io" + + ghBrowser "github.com/cli/go-gh/v2/pkg/browser" +) + +type Browser interface { + Browse(string) error +} + +func New(launcher string, stdout, stderr io.Writer) Browser { + b := ghBrowser.New(launcher, stdout, stderr) + return b +} diff --git a/vendor/github.com/cli/cli/v2/internal/browser/stub.go b/vendor/github.com/cli/cli/v2/internal/browser/stub.go new file mode 100644 index 000000000..52548affd --- /dev/null +++ b/vendor/github.com/cli/cli/v2/internal/browser/stub.go @@ -0,0 +1,40 @@ +package browser + +type Stub struct { + urls []string +} + +func (b *Stub) Browse(url string) error { + b.urls = append(b.urls, url) + return nil +} + +func (b *Stub) BrowsedURL() string { + if len(b.urls) > 0 { + return b.urls[0] + } + return "" +} + +type _testing interface { + Errorf(string, ...interface{}) + Helper() +} + +func (b *Stub) Verify(t _testing, url string) { + t.Helper() + if url != "" { + switch len(b.urls) { + case 0: + t.Errorf("expected browser to open URL %q, but it was never invoked", url) + case 1: + if url != b.urls[0] { + t.Errorf("expected browser to open URL %q, got %q", url, b.urls[0]) + } + default: + t.Errorf("expected browser to open one URL, but was invoked %d times", len(b.urls)) + } + } else if len(b.urls) > 0 { + t.Errorf("expected no browser to open, but was invoked %d times: %v", len(b.urls), b.urls) + } +} diff --git a/vendor/github.com/cli/cli/v2/internal/config/config.go b/vendor/github.com/cli/cli/v2/internal/config/config.go new file mode 100644 index 000000000..25f3e01e9 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/internal/config/config.go @@ -0,0 +1,604 @@ +package config + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "slices" + + "github.com/cli/cli/v2/internal/keyring" + ghAuth "github.com/cli/go-gh/v2/pkg/auth" + ghConfig "github.com/cli/go-gh/v2/pkg/config" +) + +const ( + aliasesKey = "aliases" + browserKey = "browser" + editorKey = "editor" + gitProtocolKey = "git_protocol" + hostsKey = "hosts" + httpUnixSocketKey = "http_unix_socket" + oauthTokenKey = "oauth_token" + pagerKey = "pager" + promptKey = "prompt" + userKey = "user" + usersKey = "users" + versionKey = "version" +) + +// This interface describes interacting with some persistent configuration for gh. +// +//go:generate moq -rm -out config_mock.go . Config +type Config interface { + GetOrDefault(string, string) (string, error) + Set(string, string, string) + Write() error + Migrate(Migration) error + + Aliases() *AliasConfig + Authentication() *AuthConfig + Browser(string) string + Editor(string) string + GitProtocol(string) string + HTTPUnixSocket(string) string + Pager(string) string + Prompt(string) string + Version() string +} + +// Migration is the interface that config migrations must implement. +// +// Migrations will receive a copy of the config, and should modify that copy +// as necessary. After migration has completed, the modified config contents +// will be used. +// +// The calling code is expected to verify that the current version of the config +// matches the PreVersion of the migration before calling Do, and will set the +// config version to the PostVersion after the migration has completed successfully. +// +//go:generate moq -rm -out migration_mock.go . Migration +type Migration interface { + // PreVersion is the required config version for this to be applied + PreVersion() string + // PostVersion is the config version that must be applied after migration + PostVersion() string + // Do is expected to apply any necessary changes to the config in place + Do(*ghConfig.Config) error +} + +func NewConfig() (Config, error) { + c, err := ghConfig.Read(fallbackConfig()) + if err != nil { + return nil, err + } + return &cfg{c}, nil +} + +// Implements Config interface +type cfg struct { + cfg *ghConfig.Config +} + +func (c *cfg) Get(hostname, key string) (string, error) { + if hostname != "" { + val, err := c.cfg.Get([]string{hostsKey, hostname, key}) + if err == nil { + return val, err + } + } + + return c.cfg.Get([]string{key}) +} + +func (c *cfg) GetOrDefault(hostname, key string) (string, error) { + val, err := c.Get(hostname, key) + if err == nil { + return val, err + } + + if val, ok := defaultFor(key); ok { + return val, nil + } + + return val, err +} + +func (c *cfg) Set(hostname, key, value string) { + if hostname == "" { + c.cfg.Set([]string{key}, value) + return + } + + c.cfg.Set([]string{hostsKey, hostname, key}, value) + + if user, _ := c.cfg.Get([]string{hostsKey, hostname, userKey}); user != "" { + c.cfg.Set([]string{hostsKey, hostname, usersKey, user, key}, value) + } +} + +func (c *cfg) Write() error { + return ghConfig.Write(c.cfg) +} + +func (c *cfg) Aliases() *AliasConfig { + return &AliasConfig{cfg: c.cfg} +} + +func (c *cfg) Authentication() *AuthConfig { + return &AuthConfig{cfg: c.cfg} +} + +func (c *cfg) Browser(hostname string) string { + val, _ := c.GetOrDefault(hostname, browserKey) + return val +} + +func (c *cfg) Editor(hostname string) string { + val, _ := c.GetOrDefault(hostname, editorKey) + return val +} + +func (c *cfg) GitProtocol(hostname string) string { + val, _ := c.GetOrDefault(hostname, gitProtocolKey) + return val +} + +func (c *cfg) HTTPUnixSocket(hostname string) string { + val, _ := c.GetOrDefault(hostname, httpUnixSocketKey) + return val +} + +func (c *cfg) Pager(hostname string) string { + val, _ := c.GetOrDefault(hostname, pagerKey) + return val +} + +func (c *cfg) Prompt(hostname string) string { + val, _ := c.GetOrDefault(hostname, promptKey) + return val +} + +func (c *cfg) Version() string { + val, _ := c.GetOrDefault("", versionKey) + return val +} + +func (c *cfg) Migrate(m Migration) error { + version := c.Version() + + // If migration has already occurred then do not attempt to migrate again. + if m.PostVersion() == version { + return nil + } + + // If migration is incompatible with current version then return an error. + if m.PreVersion() != version { + return fmt.Errorf("failed to migrate as %q pre migration version did not match config version %q", m.PreVersion(), version) + } + + if err := m.Do(c.cfg); err != nil { + return fmt.Errorf("failed to migrate config: %s", err) + } + + c.Set("", versionKey, m.PostVersion()) + + // Then write out our migrated config. + if err := c.Write(); err != nil { + return fmt.Errorf("failed to write config after migration: %s", err) + } + + return nil +} + +func defaultFor(key string) (string, bool) { + for _, co := range ConfigOptions() { + if co.Key == key { + return co.DefaultValue, true + } + } + return "", false +} + +// AuthConfig is used for interacting with some persistent configuration for gh, +// with knowledge on how to access encrypted storage when neccesarry. +// Behavior is scoped to authentication specific tasks. +type AuthConfig struct { + cfg *ghConfig.Config + defaultHostOverride func() (string, string) + hostsOverride func() []string + tokenOverride func(string) (string, string) +} + +// ActiveToken will retrieve the active auth token for the given hostname, +// searching environment variables, plain text config, and +// lastly encrypted storage. +func (c *AuthConfig) ActiveToken(hostname string) (string, string) { + if c.tokenOverride != nil { + return c.tokenOverride(hostname) + } + token, source := ghAuth.TokenFromEnvOrConfig(hostname) + if token == "" { + var err error + token, err = c.TokenFromKeyring(hostname) + if err == nil { + source = "keyring" + } + } + return token, source +} + +// HasEnvToken returns true when a token has been specified in an +// environment variable, else returns false. +func (c *AuthConfig) HasEnvToken() bool { + // This will check if there are any environment variable + // authentication tokens set for enterprise hosts. + // Any non-github.com hostname is fine here + hostname := "example.com" + if c.tokenOverride != nil { + token, _ := c.tokenOverride(hostname) + if token != "" { + return true + } + } + // TODO: This is _extremely_ knowledgeable about the implementation of TokenFromEnvOrConfig + // It has to use a hostname that is not going to be found in the hosts so that it + // can guarantee that tokens will only be returned from a set env var. + // Discussed here, but maybe worth revisiting: https://github.com/cli/cli/pull/7169#discussion_r1136979033 + token, _ := ghAuth.TokenFromEnvOrConfig(hostname) + return token != "" +} + +// SetActiveToken will override any token resolution and return the given +// token and source for all calls to ActiveToken. Use for testing purposes only. +func (c *AuthConfig) SetActiveToken(token, source string) { + c.tokenOverride = func(_ string) (string, string) { + return token, source + } +} + +// TokenFromKeyring will retrieve the auth token for the given hostname, +// only searching in encrypted storage. +func (c *AuthConfig) TokenFromKeyring(hostname string) (string, error) { + return keyring.Get(keyringServiceName(hostname), "") +} + +// TokenFromKeyringForUser will retrieve the auth token for the given hostname +// and username, only searching in encrypted storage. +// +// An empty username will return an error because the potential to return +// the currently active token under surprising cases is just too high to risk +// compared to the utility of having the function being smart. +func (c *AuthConfig) TokenFromKeyringForUser(hostname, username string) (string, error) { + if username == "" { + return "", errors.New("username cannot be blank") + } + + return keyring.Get(keyringServiceName(hostname), username) +} + +// ActiveUser will retrieve the username for the active user at the given hostname. +// This will not be accurate if the oauth token is set from an environment variable. +func (c *AuthConfig) ActiveUser(hostname string) (string, error) { + return c.cfg.Get([]string{hostsKey, hostname, userKey}) +} + +func (c *AuthConfig) Hosts() []string { + if c.hostsOverride != nil { + return c.hostsOverride() + } + return ghAuth.KnownHosts() +} + +// SetHosts will override any hosts resolution and return the given +// hosts for all calls to Hosts. Use for testing purposes only. +func (c *AuthConfig) SetHosts(hosts []string) { + c.hostsOverride = func() []string { + return hosts + } +} + +func (c *AuthConfig) DefaultHost() (string, string) { + if c.defaultHostOverride != nil { + return c.defaultHostOverride() + } + return ghAuth.DefaultHost() +} + +// SetDefaultHost will override any host resolution and return the given +// host and source for all calls to DefaultHost. Use for testing purposes only. +func (c *AuthConfig) SetDefaultHost(host, source string) { + c.defaultHostOverride = func() (string, string) { + return host, source + } +} + +// Login will set user, git protocol, and auth token for the given hostname. +// If the encrypt option is specified it will first try to store the auth token +// in encrypted storage and will fall back to the plain text config file. +func (c *AuthConfig) Login(hostname, username, token, gitProtocol string, secureStorage bool) (bool, error) { + // In this section we set up the users config + var setErr error + if secureStorage { + // Try to set the token for this user in the encrypted storage for later switching + setErr = keyring.Set(keyringServiceName(hostname), username, token) + if setErr == nil { + // Clean up the previous oauth_token from the config file, if there were one + _ = c.cfg.Remove([]string{hostsKey, hostname, usersKey, username, oauthTokenKey}) + } + } + insecureStorageUsed := false + if !secureStorage || setErr != nil { + // And set the oauth token under the user for later switching + c.cfg.Set([]string{hostsKey, hostname, usersKey, username, oauthTokenKey}, token) + insecureStorageUsed = true + } + + if gitProtocol != "" { + // Set the host level git protocol + // Although it might be expected that this is handled by switch, git protocol + // is currently a host level config and not a user level config, so any change + // will overwrite the protocol for all users on the host. + c.cfg.Set([]string{hostsKey, hostname, gitProtocolKey}, gitProtocol) + } + + // Create the username key with an empty value so it will be + // written even when there are no keys set under it. + if _, getErr := c.cfg.Get([]string{hostsKey, hostname, usersKey, username}); getErr != nil { + c.cfg.Set([]string{hostsKey, hostname, usersKey, username}, "") + } + + // Then we activate the new user + return insecureStorageUsed, c.activateUser(hostname, username) +} + +func (c *AuthConfig) SwitchUser(hostname, user string) error { + previouslyActiveUser, err := c.ActiveUser(hostname) + if err != nil { + return fmt.Errorf("failed to get active user: %s", err) + } + + previouslyActiveToken, previousSource := c.ActiveToken(hostname) + if previousSource != "keyring" && previousSource != "oauth_token" { + return fmt.Errorf("currently active token for %s is from %s", hostname, previousSource) + } + + err = c.activateUser(hostname, user) + if err != nil { + // Given that activateUser can only fail before the config is written, or when writing the config + // we know for sure that the config has not been written. However, we still should restore it back + // to its previous clean state just in case something else tries to make use of the config, or tries + // to write it again. + if previousSource == "keyring" { + if setErr := keyring.Set(keyringServiceName(hostname), "", previouslyActiveToken); setErr != nil { + err = errors.Join(err, setErr) + } + } + + if previousSource == "oauth_token" { + c.cfg.Set([]string{hostsKey, hostname, oauthTokenKey}, previouslyActiveToken) + } + c.cfg.Set([]string{hostsKey, hostname, userKey}, previouslyActiveUser) + + return err + } + + return nil +} + +// Logout will remove user, git protocol, and auth token for the given hostname. +// It will remove the auth token from the encrypted storage if it exists there. +func (c *AuthConfig) Logout(hostname, username string) error { + users := c.UsersForHost(hostname) + + // If there is only one (or zero) users, then we remove the host + // and unset the keyring tokens. + if len(users) < 2 { + _ = c.cfg.Remove([]string{hostsKey, hostname}) + _ = keyring.Delete(keyringServiceName(hostname), "") + _ = keyring.Delete(keyringServiceName(hostname), username) + return ghConfig.Write(c.cfg) + } + + // Otherwise, we remove the user from this host + _ = c.cfg.Remove([]string{hostsKey, hostname, usersKey, username}) + + // This error is ignorable because we already know there is an active user for the host + activeUser, _ := c.ActiveUser(hostname) + + // If the user we're removing isn't active, then we just write the config + if activeUser != username { + return ghConfig.Write(c.cfg) + } + + // Otherwise we get the first user in the slice that isn't the user we're removing + switchUserIdx := slices.IndexFunc(users, func(n string) bool { + return n != username + }) + + // And activate them + return c.activateUser(hostname, users[switchUserIdx]) +} + +func (c *AuthConfig) activateUser(hostname, user string) error { + // We first need to idempotently clear out any set tokens for the host + _ = keyring.Delete(keyringServiceName(hostname), "") + _ = c.cfg.Remove([]string{hostsKey, hostname, oauthTokenKey}) + + // Then we'll move the keyring token or insecure token as necessary, only one of the + // following branches should be true. + + // If there is a token in the secure keyring for the user, move it to the active slot + var tokenSwitched bool + if token, err := keyring.Get(keyringServiceName(hostname), user); err == nil { + if err = keyring.Set(keyringServiceName(hostname), "", token); err != nil { + return fmt.Errorf("failed to move active token in keyring: %v", err) + } + tokenSwitched = true + } + + // If there is a token in the insecure config for the user, move it to the active field + if token, err := c.cfg.Get([]string{hostsKey, hostname, usersKey, user, oauthTokenKey}); err == nil { + c.cfg.Set([]string{hostsKey, hostname, oauthTokenKey}, token) + tokenSwitched = true + } + + if !tokenSwitched { + return fmt.Errorf("no token found for %s", user) + } + + // Then we'll update the active user for the host + c.cfg.Set([]string{hostsKey, hostname, userKey}, user) + + return ghConfig.Write(c.cfg) +} + +func (c *AuthConfig) UsersForHost(hostname string) []string { + users, err := c.cfg.Keys([]string{hostsKey, hostname, usersKey}) + if err != nil { + return nil + } + + return users +} + +func (c *AuthConfig) TokenForUser(hostname, user string) (string, string, error) { + if token, err := keyring.Get(keyringServiceName(hostname), user); err == nil { + return token, "keyring", nil + } + + if token, err := c.cfg.Get([]string{hostsKey, hostname, usersKey, user, oauthTokenKey}); err == nil { + return token, "oauth_token", nil + } + + return "", "default", fmt.Errorf("no token found for '%s'", user) +} + +func keyringServiceName(hostname string) string { + return "gh:" + hostname +} + +type AliasConfig struct { + cfg *ghConfig.Config +} + +func (a *AliasConfig) Get(alias string) (string, error) { + return a.cfg.Get([]string{aliasesKey, alias}) +} + +func (a *AliasConfig) Add(alias, expansion string) { + a.cfg.Set([]string{aliasesKey, alias}, expansion) +} + +func (a *AliasConfig) Delete(alias string) error { + return a.cfg.Remove([]string{aliasesKey, alias}) +} + +func (a *AliasConfig) All() map[string]string { + out := map[string]string{} + keys, err := a.cfg.Keys([]string{aliasesKey}) + if err != nil { + return out + } + for _, key := range keys { + val, _ := a.cfg.Get([]string{aliasesKey, key}) + out[key] = val + } + return out +} + +func fallbackConfig() *ghConfig.Config { + return ghConfig.ReadFromString(defaultConfigStr) +} + +// The schema version in here should match the PostVersion of whatever the +// last migration we decided to run is. Therefore, if we run a new migration, +// this should be bumped. +const defaultConfigStr = ` +# The default config file, auto-generated by gh. Run 'gh environment' to learn more about +# environment variables respected by gh and their precedence. + +# The current version of the config schema +version: 1 +# What protocol to use when performing git operations. Supported values: ssh, https +git_protocol: https +# What editor gh should run when creating issues, pull requests, etc. If blank, will refer to environment. +editor: +# When to interactively prompt. This is a global config that cannot be overridden by hostname. Supported values: enabled, disabled +prompt: enabled +# A pager program to send command output to, e.g. "less". If blank, will refer to environment. Set the value to "cat" to disable the pager. +pager: +# Aliases allow you to create nicknames for gh commands +aliases: + co: pr checkout +# The path to a unix socket through which send HTTP connections. If blank, HTTP traffic will be handled by net/http.DefaultTransport. +http_unix_socket: +# What web browser gh should use when opening URLs. If blank, will refer to environment. +browser: +` + +type ConfigOption struct { + Key string + Description string + DefaultValue string + AllowedValues []string +} + +func ConfigOptions() []ConfigOption { + return []ConfigOption{ + { + Key: gitProtocolKey, + Description: "the protocol to use for git clone and push operations", + DefaultValue: "https", + AllowedValues: []string{"https", "ssh"}, + }, + { + Key: editorKey, + Description: "the text editor program to use for authoring text", + DefaultValue: "", + }, + { + Key: promptKey, + Description: "toggle interactive prompting in the terminal", + DefaultValue: "enabled", + AllowedValues: []string{"enabled", "disabled"}, + }, + { + Key: pagerKey, + Description: "the terminal pager program to send standard output to", + DefaultValue: "", + }, + { + Key: httpUnixSocketKey, + Description: "the path to a Unix socket through which to make an HTTP connection", + DefaultValue: "", + }, + { + Key: browserKey, + Description: "the web browser to use for opening URLs", + DefaultValue: "", + }, + } +} + +func HomeDirPath(subdir string) (string, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", err + } + + newPath := filepath.Join(homeDir, subdir) + return newPath, nil +} + +func StateDir() string { + return ghConfig.StateDir() +} + +func DataDir() string { + return ghConfig.DataDir() +} + +func ConfigDir() string { + return ghConfig.ConfigDir() +} diff --git a/vendor/github.com/cli/cli/v2/internal/config/config_mock.go b/vendor/github.com/cli/cli/v2/internal/config/config_mock.go new file mode 100644 index 000000000..586078a2c --- /dev/null +++ b/vendor/github.com/cli/cli/v2/internal/config/config_mock.go @@ -0,0 +1,592 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package config + +import ( + "sync" +) + +// Ensure, that ConfigMock does implement Config. +// If this is not the case, regenerate this file with moq. +var _ Config = &ConfigMock{} + +// ConfigMock is a mock implementation of Config. +// +// func TestSomethingThatUsesConfig(t *testing.T) { +// +// // make and configure a mocked Config +// mockedConfig := &ConfigMock{ +// AliasesFunc: func() *AliasConfig { +// panic("mock out the Aliases method") +// }, +// AuthenticationFunc: func() *AuthConfig { +// panic("mock out the Authentication method") +// }, +// BrowserFunc: func(s string) string { +// panic("mock out the Browser method") +// }, +// EditorFunc: func(s string) string { +// panic("mock out the Editor method") +// }, +// GetOrDefaultFunc: func(s1 string, s2 string) (string, error) { +// panic("mock out the GetOrDefault method") +// }, +// GitProtocolFunc: func(s string) string { +// panic("mock out the GitProtocol method") +// }, +// HTTPUnixSocketFunc: func(s string) string { +// panic("mock out the HTTPUnixSocket method") +// }, +// MigrateFunc: func(migration Migration) error { +// panic("mock out the Migrate method") +// }, +// PagerFunc: func(s string) string { +// panic("mock out the Pager method") +// }, +// PromptFunc: func(s string) string { +// panic("mock out the Prompt method") +// }, +// SetFunc: func(s1 string, s2 string, s3 string) { +// panic("mock out the Set method") +// }, +// VersionFunc: func() string { +// panic("mock out the Version method") +// }, +// WriteFunc: func() error { +// panic("mock out the Write method") +// }, +// } +// +// // use mockedConfig in code that requires Config +// // and then make assertions. +// +// } +type ConfigMock struct { + // AliasesFunc mocks the Aliases method. + AliasesFunc func() *AliasConfig + + // AuthenticationFunc mocks the Authentication method. + AuthenticationFunc func() *AuthConfig + + // BrowserFunc mocks the Browser method. + BrowserFunc func(s string) string + + // EditorFunc mocks the Editor method. + EditorFunc func(s string) string + + // GetOrDefaultFunc mocks the GetOrDefault method. + GetOrDefaultFunc func(s1 string, s2 string) (string, error) + + // GitProtocolFunc mocks the GitProtocol method. + GitProtocolFunc func(s string) string + + // HTTPUnixSocketFunc mocks the HTTPUnixSocket method. + HTTPUnixSocketFunc func(s string) string + + // MigrateFunc mocks the Migrate method. + MigrateFunc func(migration Migration) error + + // PagerFunc mocks the Pager method. + PagerFunc func(s string) string + + // PromptFunc mocks the Prompt method. + PromptFunc func(s string) string + + // SetFunc mocks the Set method. + SetFunc func(s1 string, s2 string, s3 string) + + // VersionFunc mocks the Version method. + VersionFunc func() string + + // WriteFunc mocks the Write method. + WriteFunc func() error + + // calls tracks calls to the methods. + calls struct { + // Aliases holds details about calls to the Aliases method. + Aliases []struct { + } + // Authentication holds details about calls to the Authentication method. + Authentication []struct { + } + // Browser holds details about calls to the Browser method. + Browser []struct { + // S is the s argument value. + S string + } + // Editor holds details about calls to the Editor method. + Editor []struct { + // S is the s argument value. + S string + } + // GetOrDefault holds details about calls to the GetOrDefault method. + GetOrDefault []struct { + // S1 is the s1 argument value. + S1 string + // S2 is the s2 argument value. + S2 string + } + // GitProtocol holds details about calls to the GitProtocol method. + GitProtocol []struct { + // S is the s argument value. + S string + } + // HTTPUnixSocket holds details about calls to the HTTPUnixSocket method. + HTTPUnixSocket []struct { + // S is the s argument value. + S string + } + // Migrate holds details about calls to the Migrate method. + Migrate []struct { + // Migration is the migration argument value. + Migration Migration + } + // Pager holds details about calls to the Pager method. + Pager []struct { + // S is the s argument value. + S string + } + // Prompt holds details about calls to the Prompt method. + Prompt []struct { + // S is the s argument value. + S string + } + // Set holds details about calls to the Set method. + Set []struct { + // S1 is the s1 argument value. + S1 string + // S2 is the s2 argument value. + S2 string + // S3 is the s3 argument value. + S3 string + } + // Version holds details about calls to the Version method. + Version []struct { + } + // Write holds details about calls to the Write method. + Write []struct { + } + } + lockAliases sync.RWMutex + lockAuthentication sync.RWMutex + lockBrowser sync.RWMutex + lockEditor sync.RWMutex + lockGetOrDefault sync.RWMutex + lockGitProtocol sync.RWMutex + lockHTTPUnixSocket sync.RWMutex + lockMigrate sync.RWMutex + lockPager sync.RWMutex + lockPrompt sync.RWMutex + lockSet sync.RWMutex + lockVersion sync.RWMutex + lockWrite sync.RWMutex +} + +// Aliases calls AliasesFunc. +func (mock *ConfigMock) Aliases() *AliasConfig { + if mock.AliasesFunc == nil { + panic("ConfigMock.AliasesFunc: method is nil but Config.Aliases was just called") + } + callInfo := struct { + }{} + mock.lockAliases.Lock() + mock.calls.Aliases = append(mock.calls.Aliases, callInfo) + mock.lockAliases.Unlock() + return mock.AliasesFunc() +} + +// AliasesCalls gets all the calls that were made to Aliases. +// Check the length with: +// +// len(mockedConfig.AliasesCalls()) +func (mock *ConfigMock) AliasesCalls() []struct { +} { + var calls []struct { + } + mock.lockAliases.RLock() + calls = mock.calls.Aliases + mock.lockAliases.RUnlock() + return calls +} + +// Authentication calls AuthenticationFunc. +func (mock *ConfigMock) Authentication() *AuthConfig { + if mock.AuthenticationFunc == nil { + panic("ConfigMock.AuthenticationFunc: method is nil but Config.Authentication was just called") + } + callInfo := struct { + }{} + mock.lockAuthentication.Lock() + mock.calls.Authentication = append(mock.calls.Authentication, callInfo) + mock.lockAuthentication.Unlock() + return mock.AuthenticationFunc() +} + +// AuthenticationCalls gets all the calls that were made to Authentication. +// Check the length with: +// +// len(mockedConfig.AuthenticationCalls()) +func (mock *ConfigMock) AuthenticationCalls() []struct { +} { + var calls []struct { + } + mock.lockAuthentication.RLock() + calls = mock.calls.Authentication + mock.lockAuthentication.RUnlock() + return calls +} + +// Browser calls BrowserFunc. +func (mock *ConfigMock) Browser(s string) string { + if mock.BrowserFunc == nil { + panic("ConfigMock.BrowserFunc: method is nil but Config.Browser was just called") + } + callInfo := struct { + S string + }{ + S: s, + } + mock.lockBrowser.Lock() + mock.calls.Browser = append(mock.calls.Browser, callInfo) + mock.lockBrowser.Unlock() + return mock.BrowserFunc(s) +} + +// BrowserCalls gets all the calls that were made to Browser. +// Check the length with: +// +// len(mockedConfig.BrowserCalls()) +func (mock *ConfigMock) BrowserCalls() []struct { + S string +} { + var calls []struct { + S string + } + mock.lockBrowser.RLock() + calls = mock.calls.Browser + mock.lockBrowser.RUnlock() + return calls +} + +// Editor calls EditorFunc. +func (mock *ConfigMock) Editor(s string) string { + if mock.EditorFunc == nil { + panic("ConfigMock.EditorFunc: method is nil but Config.Editor was just called") + } + callInfo := struct { + S string + }{ + S: s, + } + mock.lockEditor.Lock() + mock.calls.Editor = append(mock.calls.Editor, callInfo) + mock.lockEditor.Unlock() + return mock.EditorFunc(s) +} + +// EditorCalls gets all the calls that were made to Editor. +// Check the length with: +// +// len(mockedConfig.EditorCalls()) +func (mock *ConfigMock) EditorCalls() []struct { + S string +} { + var calls []struct { + S string + } + mock.lockEditor.RLock() + calls = mock.calls.Editor + mock.lockEditor.RUnlock() + return calls +} + +// GetOrDefault calls GetOrDefaultFunc. +func (mock *ConfigMock) GetOrDefault(s1 string, s2 string) (string, error) { + if mock.GetOrDefaultFunc == nil { + panic("ConfigMock.GetOrDefaultFunc: method is nil but Config.GetOrDefault was just called") + } + callInfo := struct { + S1 string + S2 string + }{ + S1: s1, + S2: s2, + } + mock.lockGetOrDefault.Lock() + mock.calls.GetOrDefault = append(mock.calls.GetOrDefault, callInfo) + mock.lockGetOrDefault.Unlock() + return mock.GetOrDefaultFunc(s1, s2) +} + +// GetOrDefaultCalls gets all the calls that were made to GetOrDefault. +// Check the length with: +// +// len(mockedConfig.GetOrDefaultCalls()) +func (mock *ConfigMock) GetOrDefaultCalls() []struct { + S1 string + S2 string +} { + var calls []struct { + S1 string + S2 string + } + mock.lockGetOrDefault.RLock() + calls = mock.calls.GetOrDefault + mock.lockGetOrDefault.RUnlock() + return calls +} + +// GitProtocol calls GitProtocolFunc. +func (mock *ConfigMock) GitProtocol(s string) string { + if mock.GitProtocolFunc == nil { + panic("ConfigMock.GitProtocolFunc: method is nil but Config.GitProtocol was just called") + } + callInfo := struct { + S string + }{ + S: s, + } + mock.lockGitProtocol.Lock() + mock.calls.GitProtocol = append(mock.calls.GitProtocol, callInfo) + mock.lockGitProtocol.Unlock() + return mock.GitProtocolFunc(s) +} + +// GitProtocolCalls gets all the calls that were made to GitProtocol. +// Check the length with: +// +// len(mockedConfig.GitProtocolCalls()) +func (mock *ConfigMock) GitProtocolCalls() []struct { + S string +} { + var calls []struct { + S string + } + mock.lockGitProtocol.RLock() + calls = mock.calls.GitProtocol + mock.lockGitProtocol.RUnlock() + return calls +} + +// HTTPUnixSocket calls HTTPUnixSocketFunc. +func (mock *ConfigMock) HTTPUnixSocket(s string) string { + if mock.HTTPUnixSocketFunc == nil { + panic("ConfigMock.HTTPUnixSocketFunc: method is nil but Config.HTTPUnixSocket was just called") + } + callInfo := struct { + S string + }{ + S: s, + } + mock.lockHTTPUnixSocket.Lock() + mock.calls.HTTPUnixSocket = append(mock.calls.HTTPUnixSocket, callInfo) + mock.lockHTTPUnixSocket.Unlock() + return mock.HTTPUnixSocketFunc(s) +} + +// HTTPUnixSocketCalls gets all the calls that were made to HTTPUnixSocket. +// Check the length with: +// +// len(mockedConfig.HTTPUnixSocketCalls()) +func (mock *ConfigMock) HTTPUnixSocketCalls() []struct { + S string +} { + var calls []struct { + S string + } + mock.lockHTTPUnixSocket.RLock() + calls = mock.calls.HTTPUnixSocket + mock.lockHTTPUnixSocket.RUnlock() + return calls +} + +// Migrate calls MigrateFunc. +func (mock *ConfigMock) Migrate(migration Migration) error { + if mock.MigrateFunc == nil { + panic("ConfigMock.MigrateFunc: method is nil but Config.Migrate was just called") + } + callInfo := struct { + Migration Migration + }{ + Migration: migration, + } + mock.lockMigrate.Lock() + mock.calls.Migrate = append(mock.calls.Migrate, callInfo) + mock.lockMigrate.Unlock() + return mock.MigrateFunc(migration) +} + +// MigrateCalls gets all the calls that were made to Migrate. +// Check the length with: +// +// len(mockedConfig.MigrateCalls()) +func (mock *ConfigMock) MigrateCalls() []struct { + Migration Migration +} { + var calls []struct { + Migration Migration + } + mock.lockMigrate.RLock() + calls = mock.calls.Migrate + mock.lockMigrate.RUnlock() + return calls +} + +// Pager calls PagerFunc. +func (mock *ConfigMock) Pager(s string) string { + if mock.PagerFunc == nil { + panic("ConfigMock.PagerFunc: method is nil but Config.Pager was just called") + } + callInfo := struct { + S string + }{ + S: s, + } + mock.lockPager.Lock() + mock.calls.Pager = append(mock.calls.Pager, callInfo) + mock.lockPager.Unlock() + return mock.PagerFunc(s) +} + +// PagerCalls gets all the calls that were made to Pager. +// Check the length with: +// +// len(mockedConfig.PagerCalls()) +func (mock *ConfigMock) PagerCalls() []struct { + S string +} { + var calls []struct { + S string + } + mock.lockPager.RLock() + calls = mock.calls.Pager + mock.lockPager.RUnlock() + return calls +} + +// Prompt calls PromptFunc. +func (mock *ConfigMock) Prompt(s string) string { + if mock.PromptFunc == nil { + panic("ConfigMock.PromptFunc: method is nil but Config.Prompt was just called") + } + callInfo := struct { + S string + }{ + S: s, + } + mock.lockPrompt.Lock() + mock.calls.Prompt = append(mock.calls.Prompt, callInfo) + mock.lockPrompt.Unlock() + return mock.PromptFunc(s) +} + +// PromptCalls gets all the calls that were made to Prompt. +// Check the length with: +// +// len(mockedConfig.PromptCalls()) +func (mock *ConfigMock) PromptCalls() []struct { + S string +} { + var calls []struct { + S string + } + mock.lockPrompt.RLock() + calls = mock.calls.Prompt + mock.lockPrompt.RUnlock() + return calls +} + +// Set calls SetFunc. +func (mock *ConfigMock) Set(s1 string, s2 string, s3 string) { + if mock.SetFunc == nil { + panic("ConfigMock.SetFunc: method is nil but Config.Set was just called") + } + callInfo := struct { + S1 string + S2 string + S3 string + }{ + S1: s1, + S2: s2, + S3: s3, + } + mock.lockSet.Lock() + mock.calls.Set = append(mock.calls.Set, callInfo) + mock.lockSet.Unlock() + mock.SetFunc(s1, s2, s3) +} + +// SetCalls gets all the calls that were made to Set. +// Check the length with: +// +// len(mockedConfig.SetCalls()) +func (mock *ConfigMock) SetCalls() []struct { + S1 string + S2 string + S3 string +} { + var calls []struct { + S1 string + S2 string + S3 string + } + mock.lockSet.RLock() + calls = mock.calls.Set + mock.lockSet.RUnlock() + return calls +} + +// Version calls VersionFunc. +func (mock *ConfigMock) Version() string { + if mock.VersionFunc == nil { + panic("ConfigMock.VersionFunc: method is nil but Config.Version was just called") + } + callInfo := struct { + }{} + mock.lockVersion.Lock() + mock.calls.Version = append(mock.calls.Version, callInfo) + mock.lockVersion.Unlock() + return mock.VersionFunc() +} + +// VersionCalls gets all the calls that were made to Version. +// Check the length with: +// +// len(mockedConfig.VersionCalls()) +func (mock *ConfigMock) VersionCalls() []struct { +} { + var calls []struct { + } + mock.lockVersion.RLock() + calls = mock.calls.Version + mock.lockVersion.RUnlock() + return calls +} + +// Write calls WriteFunc. +func (mock *ConfigMock) Write() error { + if mock.WriteFunc == nil { + panic("ConfigMock.WriteFunc: method is nil but Config.Write was just called") + } + callInfo := struct { + }{} + mock.lockWrite.Lock() + mock.calls.Write = append(mock.calls.Write, callInfo) + mock.lockWrite.Unlock() + return mock.WriteFunc() +} + +// WriteCalls gets all the calls that were made to Write. +// Check the length with: +// +// len(mockedConfig.WriteCalls()) +func (mock *ConfigMock) WriteCalls() []struct { +} { + var calls []struct { + } + mock.lockWrite.RLock() + calls = mock.calls.Write + mock.lockWrite.RUnlock() + return calls +} diff --git a/vendor/github.com/cli/cli/v2/internal/config/migration_mock.go b/vendor/github.com/cli/cli/v2/internal/config/migration_mock.go new file mode 100644 index 000000000..bf8133fb4 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/internal/config/migration_mock.go @@ -0,0 +1,149 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package config + +import ( + ghConfig "github.com/cli/go-gh/v2/pkg/config" + "sync" +) + +// Ensure, that MigrationMock does implement Migration. +// If this is not the case, regenerate this file with moq. +var _ Migration = &MigrationMock{} + +// MigrationMock is a mock implementation of Migration. +// +// func TestSomethingThatUsesMigration(t *testing.T) { +// +// // make and configure a mocked Migration +// mockedMigration := &MigrationMock{ +// DoFunc: func(config *ghConfig.Config) error { +// panic("mock out the Do method") +// }, +// PostVersionFunc: func() string { +// panic("mock out the PostVersion method") +// }, +// PreVersionFunc: func() string { +// panic("mock out the PreVersion method") +// }, +// } +// +// // use mockedMigration in code that requires Migration +// // and then make assertions. +// +// } +type MigrationMock struct { + // DoFunc mocks the Do method. + DoFunc func(config *ghConfig.Config) error + + // PostVersionFunc mocks the PostVersion method. + PostVersionFunc func() string + + // PreVersionFunc mocks the PreVersion method. + PreVersionFunc func() string + + // calls tracks calls to the methods. + calls struct { + // Do holds details about calls to the Do method. + Do []struct { + // Config is the config argument value. + Config *ghConfig.Config + } + // PostVersion holds details about calls to the PostVersion method. + PostVersion []struct { + } + // PreVersion holds details about calls to the PreVersion method. + PreVersion []struct { + } + } + lockDo sync.RWMutex + lockPostVersion sync.RWMutex + lockPreVersion sync.RWMutex +} + +// Do calls DoFunc. +func (mock *MigrationMock) Do(config *ghConfig.Config) error { + if mock.DoFunc == nil { + panic("MigrationMock.DoFunc: method is nil but Migration.Do was just called") + } + callInfo := struct { + Config *ghConfig.Config + }{ + Config: config, + } + mock.lockDo.Lock() + mock.calls.Do = append(mock.calls.Do, callInfo) + mock.lockDo.Unlock() + return mock.DoFunc(config) +} + +// DoCalls gets all the calls that were made to Do. +// Check the length with: +// +// len(mockedMigration.DoCalls()) +func (mock *MigrationMock) DoCalls() []struct { + Config *ghConfig.Config +} { + var calls []struct { + Config *ghConfig.Config + } + mock.lockDo.RLock() + calls = mock.calls.Do + mock.lockDo.RUnlock() + return calls +} + +// PostVersion calls PostVersionFunc. +func (mock *MigrationMock) PostVersion() string { + if mock.PostVersionFunc == nil { + panic("MigrationMock.PostVersionFunc: method is nil but Migration.PostVersion was just called") + } + callInfo := struct { + }{} + mock.lockPostVersion.Lock() + mock.calls.PostVersion = append(mock.calls.PostVersion, callInfo) + mock.lockPostVersion.Unlock() + return mock.PostVersionFunc() +} + +// PostVersionCalls gets all the calls that were made to PostVersion. +// Check the length with: +// +// len(mockedMigration.PostVersionCalls()) +func (mock *MigrationMock) PostVersionCalls() []struct { +} { + var calls []struct { + } + mock.lockPostVersion.RLock() + calls = mock.calls.PostVersion + mock.lockPostVersion.RUnlock() + return calls +} + +// PreVersion calls PreVersionFunc. +func (mock *MigrationMock) PreVersion() string { + if mock.PreVersionFunc == nil { + panic("MigrationMock.PreVersionFunc: method is nil but Migration.PreVersion was just called") + } + callInfo := struct { + }{} + mock.lockPreVersion.Lock() + mock.calls.PreVersion = append(mock.calls.PreVersion, callInfo) + mock.lockPreVersion.Unlock() + return mock.PreVersionFunc() +} + +// PreVersionCalls gets all the calls that were made to PreVersion. +// Check the length with: +// +// len(mockedMigration.PreVersionCalls()) +func (mock *MigrationMock) PreVersionCalls() []struct { +} { + var calls []struct { + } + mock.lockPreVersion.RLock() + calls = mock.calls.PreVersion + mock.lockPreVersion.RUnlock() + return calls +} diff --git a/vendor/github.com/cli/cli/v2/internal/config/stub.go b/vendor/github.com/cli/cli/v2/internal/config/stub.go new file mode 100644 index 000000000..98c1ceaba --- /dev/null +++ b/vendor/github.com/cli/cli/v2/internal/config/stub.go @@ -0,0 +1,150 @@ +package config + +import ( + "io" + "os" + "path/filepath" + "testing" + + "github.com/cli/cli/v2/internal/keyring" + ghConfig "github.com/cli/go-gh/v2/pkg/config" +) + +func NewBlankConfig() *ConfigMock { + return NewFromString(defaultConfigStr) +} + +func NewFromString(cfgStr string) *ConfigMock { + c := ghConfig.ReadFromString(cfgStr) + cfg := cfg{c} + mock := &ConfigMock{} + mock.GetOrDefaultFunc = func(host, key string) (string, error) { + return cfg.GetOrDefault(host, key) + } + mock.SetFunc = func(host, key, value string) { + cfg.Set(host, key, value) + } + mock.WriteFunc = func() error { + return cfg.Write() + } + mock.MigrateFunc = func(m Migration) error { + return cfg.Migrate(m) + } + mock.AliasesFunc = func() *AliasConfig { + return &AliasConfig{cfg: c} + } + mock.AuthenticationFunc = func() *AuthConfig { + return &AuthConfig{ + cfg: c, + defaultHostOverride: func() (string, string) { + return "github.com", "default" + }, + hostsOverride: func() []string { + keys, _ := c.Keys([]string{hostsKey}) + return keys + }, + tokenOverride: func(hostname string) (string, string) { + token, _ := c.Get([]string{hostsKey, hostname, oauthTokenKey}) + return token, oauthTokenKey + }, + } + } + mock.BrowserFunc = func(hostname string) string { + val, _ := cfg.GetOrDefault(hostname, browserKey) + return val + } + mock.EditorFunc = func(hostname string) string { + val, _ := cfg.GetOrDefault(hostname, editorKey) + return val + } + mock.GitProtocolFunc = func(hostname string) string { + val, _ := cfg.GetOrDefault(hostname, gitProtocolKey) + return val + } + mock.HTTPUnixSocketFunc = func(hostname string) string { + val, _ := cfg.GetOrDefault(hostname, httpUnixSocketKey) + return val + } + mock.PagerFunc = func(hostname string) string { + val, _ := cfg.GetOrDefault(hostname, pagerKey) + return val + } + mock.PromptFunc = func(hostname string) string { + val, _ := cfg.GetOrDefault(hostname, promptKey) + return val + } + mock.VersionFunc = func() string { + val, _ := cfg.GetOrDefault("", versionKey) + return val + } + return mock +} + +// NewIsolatedTestConfig sets up a Mock keyring, creates a blank config +// overwrites the ghConfig.Read function that returns a singleton config +// in the real implementation, sets the GH_CONFIG_DIR env var so that +// any call to Write goes to a different location on disk, and then returns +// the blank config and a function that reads any data written to disk. +func NewIsolatedTestConfig(t *testing.T) (Config, func(io.Writer, io.Writer)) { + keyring.MockInit() + + c := ghConfig.ReadFromString("") + cfg := cfg{c} + + // The real implementation of config.Read uses a sync.Once + // to read config files and initialise package level variables + // that are used from then on. + // + // This means that tests can't be isolated from each other, so + // we swap out the function here to return a new config each time. + ghConfig.Read = func(_ *ghConfig.Config) (*ghConfig.Config, error) { + return c, nil + } + + // The config.Write method isn't defined in the same way as Read to allow + // the function to be swapped out and it does try to write to disk. + // + // We should consider whether it makes sense to change that but in the meantime + // we can use GH_CONFIG_DIR env var to ensure the tests remain isolated. + readConfigs := StubWriteConfig(t) + + return &cfg, readConfigs +} + +// StubWriteConfig stubs out the filesystem where config file are written. +// It then returns a function that will read in the config files into io.Writers. +// It automatically cleans up environment variables and written files. +func StubWriteConfig(t *testing.T) func(io.Writer, io.Writer) { + t.Helper() + tempDir := t.TempDir() + t.Setenv("GH_CONFIG_DIR", tempDir) + return func(wc io.Writer, wh io.Writer) { + config, err := os.Open(filepath.Join(tempDir, "config.yml")) + if err != nil { + return + } + defer config.Close() + configData, err := io.ReadAll(config) + if err != nil { + return + } + _, err = wc.Write(configData) + if err != nil { + return + } + + hosts, err := os.Open(filepath.Join(tempDir, "hosts.yml")) + if err != nil { + return + } + defer hosts.Close() + hostsData, err := io.ReadAll(hosts) + if err != nil { + return + } + _, err = wh.Write(hostsData) + if err != nil { + return + } + } +} diff --git a/vendor/github.com/cli/cli/v2/internal/ghinstance/host.go b/vendor/github.com/cli/cli/v2/internal/ghinstance/host.go new file mode 100644 index 000000000..729d47f31 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/internal/ghinstance/host.go @@ -0,0 +1,132 @@ +package ghinstance + +import ( + "errors" + "fmt" + "strings" +) + +// DefaultHostname is the domain name of the default GitHub instance. +const defaultHostname = "github.com" + +// Localhost is the domain name of a local GitHub instance. +const localhost = "github.localhost" + +// TenancyHost is the domain name of a tenancy GitHub instance. +const tenancyHost = "ghe.com" + +// Default returns the host name of the default GitHub instance. +func Default() string { + return defaultHostname +} + +// IsEnterprise reports whether a non-normalized host name looks like a GHE instance. +func IsEnterprise(h string) bool { + normalizedHostName := NormalizeHostname(h) + return normalizedHostName != defaultHostname && normalizedHostName != localhost +} + +// IsTenancy reports whether a non-normalized host name looks like a tenancy instance. +func IsTenancy(h string) bool { + normalizedHostName := NormalizeHostname(h) + return strings.HasSuffix(normalizedHostName, "."+tenancyHost) +} + +// TenantName extracts the tenant name from tenancy host name and +// reports whether it found the tenant name. +func TenantName(h string) (string, bool) { + normalizedHostName := NormalizeHostname(h) + return cutSuffix(normalizedHostName, "."+tenancyHost) +} + +func isGarage(h string) bool { + return strings.EqualFold(h, "garage.github.com") +} + +// NormalizeHostname returns the canonical host name of a GitHub instance. +func NormalizeHostname(h string) string { + hostname := strings.ToLower(h) + if strings.HasSuffix(hostname, "."+defaultHostname) { + return defaultHostname + } + if strings.HasSuffix(hostname, "."+localhost) { + return localhost + } + if before, found := cutSuffix(hostname, "."+tenancyHost); found { + idx := strings.LastIndex(before, ".") + return fmt.Sprintf("%s.%s", before[idx+1:], tenancyHost) + } + return hostname +} + +func HostnameValidator(hostname string) error { + if len(strings.TrimSpace(hostname)) < 1 { + return errors.New("a value is required") + } + if strings.ContainsRune(hostname, '/') || strings.ContainsRune(hostname, ':') { + return errors.New("invalid hostname") + } + return nil +} + +func GraphQLEndpoint(hostname string) string { + if isGarage(hostname) { + return fmt.Sprintf("https://%s/api/graphql", hostname) + } + if IsEnterprise(hostname) { + return fmt.Sprintf("https://%s/api/graphql", hostname) + } + if strings.EqualFold(hostname, localhost) { + return fmt.Sprintf("http://api.%s/graphql", hostname) + } + return fmt.Sprintf("https://api.%s/graphql", hostname) +} + +func RESTPrefix(hostname string) string { + if isGarage(hostname) { + return fmt.Sprintf("https://%s/api/v3/", hostname) + } + if IsEnterprise(hostname) { + return fmt.Sprintf("https://%s/api/v3/", hostname) + } + if strings.EqualFold(hostname, localhost) { + return fmt.Sprintf("http://api.%s/", hostname) + } + return fmt.Sprintf("https://api.%s/", hostname) +} + +func GistPrefix(hostname string) string { + prefix := "https://" + if strings.EqualFold(hostname, localhost) { + prefix = "http://" + } + return prefix + GistHost(hostname) +} + +func GistHost(hostname string) string { + if isGarage(hostname) { + return fmt.Sprintf("%s/gist/", hostname) + } + if IsEnterprise(hostname) { + return fmt.Sprintf("%s/gist/", hostname) + } + if strings.EqualFold(hostname, localhost) { + return fmt.Sprintf("%s/gist/", hostname) + } + return fmt.Sprintf("gist.%s/", hostname) +} + +func HostPrefix(hostname string) string { + if strings.EqualFold(hostname, localhost) { + return fmt.Sprintf("http://%s/", hostname) + } + return fmt.Sprintf("https://%s/", hostname) +} + +// Backport strings.CutSuffix from Go 1.20. +func cutSuffix(s, suffix string) (string, bool) { + if !strings.HasSuffix(s, suffix) { + return s, false + } + return s[:len(s)-len(suffix)], true +} diff --git a/vendor/github.com/cli/cli/v2/internal/ghrepo/repo.go b/vendor/github.com/cli/cli/v2/internal/ghrepo/repo.go new file mode 100644 index 000000000..4f0328e99 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/internal/ghrepo/repo.go @@ -0,0 +1,121 @@ +package ghrepo + +import ( + "fmt" + "net/url" + "strings" + + "github.com/cli/cli/v2/internal/ghinstance" + ghAuth "github.com/cli/go-gh/v2/pkg/auth" + "github.com/cli/go-gh/v2/pkg/repository" +) + +// Interface describes an object that represents a GitHub repository +type Interface interface { + RepoName() string + RepoOwner() string + RepoHost() string +} + +// New instantiates a GitHub repository from owner and name arguments +func New(owner, repo string) Interface { + return NewWithHost(owner, repo, ghinstance.Default()) +} + +// NewWithHost is like New with an explicit host name +func NewWithHost(owner, repo, hostname string) Interface { + return &ghRepo{ + owner: owner, + name: repo, + hostname: normalizeHostname(hostname), + } +} + +// FullName serializes a GitHub repository into an "OWNER/REPO" string +func FullName(r Interface) string { + return fmt.Sprintf("%s/%s", r.RepoOwner(), r.RepoName()) +} + +func defaultHost() string { + host, _ := ghAuth.DefaultHost() + return host +} + +// FromFullName extracts the GitHub repository information from the following +// formats: "OWNER/REPO", "HOST/OWNER/REPO", and a full URL. +func FromFullName(nwo string) (Interface, error) { + return FromFullNameWithHost(nwo, defaultHost()) +} + +// FromFullNameWithHost is like FromFullName that defaults to a specific host for values that don't +// explicitly include a hostname. +func FromFullNameWithHost(nwo, fallbackHost string) (Interface, error) { + repo, err := repository.ParseWithHost(nwo, fallbackHost) + if err != nil { + return nil, err + } + return NewWithHost(repo.Owner, repo.Name, repo.Host), nil +} + +// FromURL extracts the GitHub repository information from a git remote URL +func FromURL(u *url.URL) (Interface, error) { + if u.Hostname() == "" { + return nil, fmt.Errorf("no hostname detected") + } + + parts := strings.SplitN(strings.Trim(u.Path, "/"), "/", 3) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid path: %s", u.Path) + } + + return NewWithHost(parts[0], strings.TrimSuffix(parts[1], ".git"), u.Hostname()), nil +} + +func normalizeHostname(h string) string { + return strings.ToLower(strings.TrimPrefix(h, "www.")) +} + +// IsSame compares two GitHub repositories +func IsSame(a, b Interface) bool { + return strings.EqualFold(a.RepoOwner(), b.RepoOwner()) && + strings.EqualFold(a.RepoName(), b.RepoName()) && + normalizeHostname(a.RepoHost()) == normalizeHostname(b.RepoHost()) +} + +func GenerateRepoURL(repo Interface, p string, args ...interface{}) string { + baseURL := fmt.Sprintf("%s%s/%s", ghinstance.HostPrefix(repo.RepoHost()), repo.RepoOwner(), repo.RepoName()) + if p != "" { + if path := fmt.Sprintf(p, args...); path != "" { + return baseURL + "/" + path + } + } + return baseURL +} + +func FormatRemoteURL(repo Interface, protocol string) string { + if protocol == "ssh" { + if tenant, found := ghinstance.TenantName(repo.RepoHost()); found { + return fmt.Sprintf("%s@%s:%s/%s.git", tenant, repo.RepoHost(), repo.RepoOwner(), repo.RepoName()) + } + return fmt.Sprintf("git@%s:%s/%s.git", repo.RepoHost(), repo.RepoOwner(), repo.RepoName()) + } + return fmt.Sprintf("%s%s/%s.git", ghinstance.HostPrefix(repo.RepoHost()), repo.RepoOwner(), repo.RepoName()) +} + +type ghRepo struct { + owner string + name string + hostname string +} + +func (r ghRepo) RepoOwner() string { + return r.owner +} + +func (r ghRepo) RepoName() string { + return r.name +} + +func (r ghRepo) RepoHost() string { + return r.hostname +} diff --git a/vendor/github.com/cli/cli/v2/internal/keyring/keyring.go b/vendor/github.com/cli/cli/v2/internal/keyring/keyring.go new file mode 100644 index 000000000..f873c6436 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/internal/keyring/keyring.go @@ -0,0 +1,82 @@ +// Package keyring is a simple wrapper that adds timeouts to the zalando/go-keyring package. +package keyring + +import ( + "errors" + "time" + + "github.com/zalando/go-keyring" +) + +var ErrNotFound = errors.New("secret not found in keyring") + +type TimeoutError struct { + message string +} + +func (e *TimeoutError) Error() string { + return e.message +} + +// Set secret in keyring for user. +func Set(service, user, secret string) error { + ch := make(chan error, 1) + go func() { + defer close(ch) + ch <- keyring.Set(service, user, secret) + }() + select { + case err := <-ch: + return err + case <-time.After(3 * time.Second): + return &TimeoutError{"timeout while trying to set secret in keyring"} + } +} + +// Get secret from keyring given service and user name. +func Get(service, user string) (string, error) { + ch := make(chan struct { + val string + err error + }, 1) + go func() { + defer close(ch) + val, err := keyring.Get(service, user) + ch <- struct { + val string + err error + }{val, err} + }() + select { + case res := <-ch: + if errors.Is(res.err, keyring.ErrNotFound) { + return "", ErrNotFound + } + return res.val, res.err + case <-time.After(3 * time.Second): + return "", &TimeoutError{"timeout while trying to get secret from keyring"} + } +} + +// Delete secret from keyring. +func Delete(service, user string) error { + ch := make(chan error, 1) + go func() { + defer close(ch) + ch <- keyring.Delete(service, user) + }() + select { + case err := <-ch: + return err + case <-time.After(3 * time.Second): + return &TimeoutError{"timeout while trying to delete secret from keyring"} + } +} + +func MockInit() { + keyring.MockInit() +} + +func MockInitWithError(err error) { + keyring.MockInitWithError(err) +} diff --git a/vendor/github.com/cli/cli/v2/internal/prompter/prompter.go b/vendor/github.com/cli/cli/v2/internal/prompter/prompter.go new file mode 100644 index 000000000..44579b189 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/internal/prompter/prompter.go @@ -0,0 +1,125 @@ +package prompter + +import ( + "fmt" + "strings" + + "github.com/AlecAivazis/survey/v2" + "github.com/cli/cli/v2/internal/ghinstance" + "github.com/cli/cli/v2/pkg/surveyext" + ghPrompter "github.com/cli/go-gh/v2/pkg/prompter" +) + +//go:generate moq -rm -out prompter_mock.go . Prompter +type Prompter interface { + // generic prompts from go-gh + Select(string, string, []string) (int, error) + MultiSelect(prompt string, defaults []string, options []string) ([]int, error) + Input(string, string) (string, error) + Password(string) (string, error) + Confirm(string, bool) (bool, error) + + // gh specific prompts + AuthToken() (string, error) + ConfirmDeletion(string) error + InputHostname() (string, error) + MarkdownEditor(string, string, bool) (string, error) +} + +func New(editorCmd string, stdin ghPrompter.FileReader, stdout ghPrompter.FileWriter, stderr ghPrompter.FileWriter) Prompter { + return &surveyPrompter{ + prompter: ghPrompter.New(stdin, stdout, stderr), + stdin: stdin, + stdout: stdout, + stderr: stderr, + editorCmd: editorCmd, + } +} + +type surveyPrompter struct { + prompter *ghPrompter.Prompter + stdin ghPrompter.FileReader + stdout ghPrompter.FileWriter + stderr ghPrompter.FileWriter + editorCmd string +} + +func (p *surveyPrompter) Select(prompt, defaultValue string, options []string) (int, error) { + return p.prompter.Select(prompt, defaultValue, options) +} + +func (p *surveyPrompter) MultiSelect(prompt string, defaultValues, options []string) ([]int, error) { + return p.prompter.MultiSelect(prompt, defaultValues, options) +} + +func (p *surveyPrompter) Input(prompt, defaultValue string) (string, error) { + return p.prompter.Input(prompt, defaultValue) +} + +func (p *surveyPrompter) Password(prompt string) (string, error) { + return p.prompter.Password(prompt) +} + +func (p *surveyPrompter) Confirm(prompt string, defaultValue bool) (bool, error) { + return p.prompter.Confirm(prompt, defaultValue) +} + +func (p *surveyPrompter) AuthToken() (string, error) { + var result string + err := p.ask(&survey.Password{ + Message: "Paste your authentication token:", + }, &result, survey.WithValidator(survey.Required)) + return result, err +} + +func (p *surveyPrompter) ConfirmDeletion(requiredValue string) error { + var result string + return p.ask( + &survey.Input{ + Message: fmt.Sprintf("Type %s to confirm deletion:", requiredValue), + }, + &result, + survey.WithValidator( + func(val interface{}) error { + if str := val.(string); !strings.EqualFold(str, requiredValue) { + return fmt.Errorf("You entered %s", str) + } + return nil + })) +} + +func (p *surveyPrompter) InputHostname() (string, error) { + var result string + err := p.ask( + &survey.Input{ + Message: "GHE hostname:", + }, &result, survey.WithValidator(func(v interface{}) error { + return ghinstance.HostnameValidator(v.(string)) + })) + return result, err +} + +func (p *surveyPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) { + var result string + err := p.ask(&surveyext.GhEditor{ + BlankAllowed: blankAllowed, + EditorCommand: p.editorCmd, + Editor: &survey.Editor{ + Message: prompt, + Default: defaultValue, + FileName: "*.md", + HideDefault: true, + AppendDefault: true, + }, + }, &result) + return result, err +} + +func (p *surveyPrompter) ask(q survey.Prompt, response interface{}, opts ...survey.AskOpt) error { + opts = append(opts, survey.WithStdio(p.stdin, p.stdout, p.stderr)) + err := survey.AskOne(q, response, opts...) + if err == nil { + return nil + } + return fmt.Errorf("could not prompt: %w", err) +} diff --git a/vendor/github.com/cli/cli/v2/internal/prompter/prompter_mock.go b/vendor/github.com/cli/cli/v2/internal/prompter/prompter_mock.go new file mode 100644 index 000000000..b817a491f --- /dev/null +++ b/vendor/github.com/cli/cli/v2/internal/prompter/prompter_mock.go @@ -0,0 +1,460 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package prompter + +import ( + "sync" +) + +// Ensure, that PrompterMock does implement Prompter. +// If this is not the case, regenerate this file with moq. +var _ Prompter = &PrompterMock{} + +// PrompterMock is a mock implementation of Prompter. +// +// func TestSomethingThatUsesPrompter(t *testing.T) { +// +// // make and configure a mocked Prompter +// mockedPrompter := &PrompterMock{ +// AuthTokenFunc: func() (string, error) { +// panic("mock out the AuthToken method") +// }, +// ConfirmFunc: func(s string, b bool) (bool, error) { +// panic("mock out the Confirm method") +// }, +// ConfirmDeletionFunc: func(s string) error { +// panic("mock out the ConfirmDeletion method") +// }, +// InputFunc: func(s1 string, s2 string) (string, error) { +// panic("mock out the Input method") +// }, +// InputHostnameFunc: func() (string, error) { +// panic("mock out the InputHostname method") +// }, +// MarkdownEditorFunc: func(s1 string, s2 string, b bool) (string, error) { +// panic("mock out the MarkdownEditor method") +// }, +// MultiSelectFunc: func(prompt string, defaults []string, options []string) ([]int, error) { +// panic("mock out the MultiSelect method") +// }, +// PasswordFunc: func(s string) (string, error) { +// panic("mock out the Password method") +// }, +// SelectFunc: func(s1 string, s2 string, strings []string) (int, error) { +// panic("mock out the Select method") +// }, +// } +// +// // use mockedPrompter in code that requires Prompter +// // and then make assertions. +// +// } +type PrompterMock struct { + // AuthTokenFunc mocks the AuthToken method. + AuthTokenFunc func() (string, error) + + // ConfirmFunc mocks the Confirm method. + ConfirmFunc func(s string, b bool) (bool, error) + + // ConfirmDeletionFunc mocks the ConfirmDeletion method. + ConfirmDeletionFunc func(s string) error + + // InputFunc mocks the Input method. + InputFunc func(s1 string, s2 string) (string, error) + + // InputHostnameFunc mocks the InputHostname method. + InputHostnameFunc func() (string, error) + + // MarkdownEditorFunc mocks the MarkdownEditor method. + MarkdownEditorFunc func(s1 string, s2 string, b bool) (string, error) + + // MultiSelectFunc mocks the MultiSelect method. + MultiSelectFunc func(prompt string, defaults []string, options []string) ([]int, error) + + // PasswordFunc mocks the Password method. + PasswordFunc func(s string) (string, error) + + // SelectFunc mocks the Select method. + SelectFunc func(s1 string, s2 string, strings []string) (int, error) + + // calls tracks calls to the methods. + calls struct { + // AuthToken holds details about calls to the AuthToken method. + AuthToken []struct { + } + // Confirm holds details about calls to the Confirm method. + Confirm []struct { + // S is the s argument value. + S string + // B is the b argument value. + B bool + } + // ConfirmDeletion holds details about calls to the ConfirmDeletion method. + ConfirmDeletion []struct { + // S is the s argument value. + S string + } + // Input holds details about calls to the Input method. + Input []struct { + // S1 is the s1 argument value. + S1 string + // S2 is the s2 argument value. + S2 string + } + // InputHostname holds details about calls to the InputHostname method. + InputHostname []struct { + } + // MarkdownEditor holds details about calls to the MarkdownEditor method. + MarkdownEditor []struct { + // S1 is the s1 argument value. + S1 string + // S2 is the s2 argument value. + S2 string + // B is the b argument value. + B bool + } + // MultiSelect holds details about calls to the MultiSelect method. + MultiSelect []struct { + // Prompt is the prompt argument value. + Prompt string + // Defaults is the defaults argument value. + Defaults []string + // Options is the options argument value. + Options []string + } + // Password holds details about calls to the Password method. + Password []struct { + // S is the s argument value. + S string + } + // Select holds details about calls to the Select method. + Select []struct { + // S1 is the s1 argument value. + S1 string + // S2 is the s2 argument value. + S2 string + // Strings is the strings argument value. + Strings []string + } + } + lockAuthToken sync.RWMutex + lockConfirm sync.RWMutex + lockConfirmDeletion sync.RWMutex + lockInput sync.RWMutex + lockInputHostname sync.RWMutex + lockMarkdownEditor sync.RWMutex + lockMultiSelect sync.RWMutex + lockPassword sync.RWMutex + lockSelect sync.RWMutex +} + +// AuthToken calls AuthTokenFunc. +func (mock *PrompterMock) AuthToken() (string, error) { + if mock.AuthTokenFunc == nil { + panic("PrompterMock.AuthTokenFunc: method is nil but Prompter.AuthToken was just called") + } + callInfo := struct { + }{} + mock.lockAuthToken.Lock() + mock.calls.AuthToken = append(mock.calls.AuthToken, callInfo) + mock.lockAuthToken.Unlock() + return mock.AuthTokenFunc() +} + +// AuthTokenCalls gets all the calls that were made to AuthToken. +// Check the length with: +// +// len(mockedPrompter.AuthTokenCalls()) +func (mock *PrompterMock) AuthTokenCalls() []struct { +} { + var calls []struct { + } + mock.lockAuthToken.RLock() + calls = mock.calls.AuthToken + mock.lockAuthToken.RUnlock() + return calls +} + +// Confirm calls ConfirmFunc. +func (mock *PrompterMock) Confirm(s string, b bool) (bool, error) { + if mock.ConfirmFunc == nil { + panic("PrompterMock.ConfirmFunc: method is nil but Prompter.Confirm was just called") + } + callInfo := struct { + S string + B bool + }{ + S: s, + B: b, + } + mock.lockConfirm.Lock() + mock.calls.Confirm = append(mock.calls.Confirm, callInfo) + mock.lockConfirm.Unlock() + return mock.ConfirmFunc(s, b) +} + +// ConfirmCalls gets all the calls that were made to Confirm. +// Check the length with: +// +// len(mockedPrompter.ConfirmCalls()) +func (mock *PrompterMock) ConfirmCalls() []struct { + S string + B bool +} { + var calls []struct { + S string + B bool + } + mock.lockConfirm.RLock() + calls = mock.calls.Confirm + mock.lockConfirm.RUnlock() + return calls +} + +// ConfirmDeletion calls ConfirmDeletionFunc. +func (mock *PrompterMock) ConfirmDeletion(s string) error { + if mock.ConfirmDeletionFunc == nil { + panic("PrompterMock.ConfirmDeletionFunc: method is nil but Prompter.ConfirmDeletion was just called") + } + callInfo := struct { + S string + }{ + S: s, + } + mock.lockConfirmDeletion.Lock() + mock.calls.ConfirmDeletion = append(mock.calls.ConfirmDeletion, callInfo) + mock.lockConfirmDeletion.Unlock() + return mock.ConfirmDeletionFunc(s) +} + +// ConfirmDeletionCalls gets all the calls that were made to ConfirmDeletion. +// Check the length with: +// +// len(mockedPrompter.ConfirmDeletionCalls()) +func (mock *PrompterMock) ConfirmDeletionCalls() []struct { + S string +} { + var calls []struct { + S string + } + mock.lockConfirmDeletion.RLock() + calls = mock.calls.ConfirmDeletion + mock.lockConfirmDeletion.RUnlock() + return calls +} + +// Input calls InputFunc. +func (mock *PrompterMock) Input(s1 string, s2 string) (string, error) { + if mock.InputFunc == nil { + panic("PrompterMock.InputFunc: method is nil but Prompter.Input was just called") + } + callInfo := struct { + S1 string + S2 string + }{ + S1: s1, + S2: s2, + } + mock.lockInput.Lock() + mock.calls.Input = append(mock.calls.Input, callInfo) + mock.lockInput.Unlock() + return mock.InputFunc(s1, s2) +} + +// InputCalls gets all the calls that were made to Input. +// Check the length with: +// +// len(mockedPrompter.InputCalls()) +func (mock *PrompterMock) InputCalls() []struct { + S1 string + S2 string +} { + var calls []struct { + S1 string + S2 string + } + mock.lockInput.RLock() + calls = mock.calls.Input + mock.lockInput.RUnlock() + return calls +} + +// InputHostname calls InputHostnameFunc. +func (mock *PrompterMock) InputHostname() (string, error) { + if mock.InputHostnameFunc == nil { + panic("PrompterMock.InputHostnameFunc: method is nil but Prompter.InputHostname was just called") + } + callInfo := struct { + }{} + mock.lockInputHostname.Lock() + mock.calls.InputHostname = append(mock.calls.InputHostname, callInfo) + mock.lockInputHostname.Unlock() + return mock.InputHostnameFunc() +} + +// InputHostnameCalls gets all the calls that were made to InputHostname. +// Check the length with: +// +// len(mockedPrompter.InputHostnameCalls()) +func (mock *PrompterMock) InputHostnameCalls() []struct { +} { + var calls []struct { + } + mock.lockInputHostname.RLock() + calls = mock.calls.InputHostname + mock.lockInputHostname.RUnlock() + return calls +} + +// MarkdownEditor calls MarkdownEditorFunc. +func (mock *PrompterMock) MarkdownEditor(s1 string, s2 string, b bool) (string, error) { + if mock.MarkdownEditorFunc == nil { + panic("PrompterMock.MarkdownEditorFunc: method is nil but Prompter.MarkdownEditor was just called") + } + callInfo := struct { + S1 string + S2 string + B bool + }{ + S1: s1, + S2: s2, + B: b, + } + mock.lockMarkdownEditor.Lock() + mock.calls.MarkdownEditor = append(mock.calls.MarkdownEditor, callInfo) + mock.lockMarkdownEditor.Unlock() + return mock.MarkdownEditorFunc(s1, s2, b) +} + +// MarkdownEditorCalls gets all the calls that were made to MarkdownEditor. +// Check the length with: +// +// len(mockedPrompter.MarkdownEditorCalls()) +func (mock *PrompterMock) MarkdownEditorCalls() []struct { + S1 string + S2 string + B bool +} { + var calls []struct { + S1 string + S2 string + B bool + } + mock.lockMarkdownEditor.RLock() + calls = mock.calls.MarkdownEditor + mock.lockMarkdownEditor.RUnlock() + return calls +} + +// MultiSelect calls MultiSelectFunc. +func (mock *PrompterMock) MultiSelect(prompt string, defaults []string, options []string) ([]int, error) { + if mock.MultiSelectFunc == nil { + panic("PrompterMock.MultiSelectFunc: method is nil but Prompter.MultiSelect was just called") + } + callInfo := struct { + Prompt string + Defaults []string + Options []string + }{ + Prompt: prompt, + Defaults: defaults, + Options: options, + } + mock.lockMultiSelect.Lock() + mock.calls.MultiSelect = append(mock.calls.MultiSelect, callInfo) + mock.lockMultiSelect.Unlock() + return mock.MultiSelectFunc(prompt, defaults, options) +} + +// MultiSelectCalls gets all the calls that were made to MultiSelect. +// Check the length with: +// +// len(mockedPrompter.MultiSelectCalls()) +func (mock *PrompterMock) MultiSelectCalls() []struct { + Prompt string + Defaults []string + Options []string +} { + var calls []struct { + Prompt string + Defaults []string + Options []string + } + mock.lockMultiSelect.RLock() + calls = mock.calls.MultiSelect + mock.lockMultiSelect.RUnlock() + return calls +} + +// Password calls PasswordFunc. +func (mock *PrompterMock) Password(s string) (string, error) { + if mock.PasswordFunc == nil { + panic("PrompterMock.PasswordFunc: method is nil but Prompter.Password was just called") + } + callInfo := struct { + S string + }{ + S: s, + } + mock.lockPassword.Lock() + mock.calls.Password = append(mock.calls.Password, callInfo) + mock.lockPassword.Unlock() + return mock.PasswordFunc(s) +} + +// PasswordCalls gets all the calls that were made to Password. +// Check the length with: +// +// len(mockedPrompter.PasswordCalls()) +func (mock *PrompterMock) PasswordCalls() []struct { + S string +} { + var calls []struct { + S string + } + mock.lockPassword.RLock() + calls = mock.calls.Password + mock.lockPassword.RUnlock() + return calls +} + +// Select calls SelectFunc. +func (mock *PrompterMock) Select(s1 string, s2 string, strings []string) (int, error) { + if mock.SelectFunc == nil { + panic("PrompterMock.SelectFunc: method is nil but Prompter.Select was just called") + } + callInfo := struct { + S1 string + S2 string + Strings []string + }{ + S1: s1, + S2: s2, + Strings: strings, + } + mock.lockSelect.Lock() + mock.calls.Select = append(mock.calls.Select, callInfo) + mock.lockSelect.Unlock() + return mock.SelectFunc(s1, s2, strings) +} + +// SelectCalls gets all the calls that were made to Select. +// Check the length with: +// +// len(mockedPrompter.SelectCalls()) +func (mock *PrompterMock) SelectCalls() []struct { + S1 string + S2 string + Strings []string +} { + var calls []struct { + S1 string + S2 string + Strings []string + } + mock.lockSelect.RLock() + calls = mock.calls.Select + mock.lockSelect.RUnlock() + return calls +} diff --git a/vendor/github.com/cli/cli/v2/internal/prompter/test.go b/vendor/github.com/cli/cli/v2/internal/prompter/test.go new file mode 100644 index 000000000..04375ce76 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/internal/prompter/test.go @@ -0,0 +1,150 @@ +package prompter + +import ( + "fmt" + "strings" + "testing" + + ghPrompter "github.com/cli/go-gh/v2/pkg/prompter" + "github.com/stretchr/testify/assert" +) + +func NewMockPrompter(t *testing.T) *MockPrompter { + m := &MockPrompter{ + t: t, + PrompterMock: *ghPrompter.NewMock(t), + authTokenStubs: []authTokenStub{}, + confirmDeletionStubs: []confirmDeletionStub{}, + inputHostnameStubs: []inputHostnameStub{}, + markdownEditorStubs: []markdownEditorStub{}, + } + t.Cleanup(m.Verify) + return m +} + +type MockPrompter struct { + t *testing.T + ghPrompter.PrompterMock + authTokenStubs []authTokenStub + confirmDeletionStubs []confirmDeletionStub + inputHostnameStubs []inputHostnameStub + markdownEditorStubs []markdownEditorStub +} + +type authTokenStub struct { + fn func() (string, error) +} + +type confirmDeletionStub struct { + prompt string + fn func(string) error +} + +type inputHostnameStub struct { + fn func() (string, error) +} + +type markdownEditorStub struct { + prompt string + fn func(string, string, bool) (string, error) +} + +func (m *MockPrompter) AuthToken() (string, error) { + var s authTokenStub + if len(m.authTokenStubs) == 0 { + return "", NoSuchPromptErr("AuthToken") + } + s = m.authTokenStubs[0] + m.authTokenStubs = m.authTokenStubs[1:len(m.authTokenStubs)] + return s.fn() +} + +func (m *MockPrompter) ConfirmDeletion(prompt string) error { + var s confirmDeletionStub + if len(m.confirmDeletionStubs) == 0 { + return NoSuchPromptErr("ConfirmDeletion") + } + s = m.confirmDeletionStubs[0] + m.confirmDeletionStubs = m.confirmDeletionStubs[1:len(m.confirmDeletionStubs)] + return s.fn(prompt) +} + +func (m *MockPrompter) InputHostname() (string, error) { + var s inputHostnameStub + if len(m.inputHostnameStubs) == 0 { + return "", NoSuchPromptErr("InputHostname") + } + s = m.inputHostnameStubs[0] + m.inputHostnameStubs = m.inputHostnameStubs[1:len(m.inputHostnameStubs)] + return s.fn() +} + +func (m *MockPrompter) MarkdownEditor(prompt, defaultValue string, blankAllowed bool) (string, error) { + var s markdownEditorStub + if len(m.markdownEditorStubs) == 0 { + return "", NoSuchPromptErr(prompt) + } + s = m.markdownEditorStubs[0] + m.markdownEditorStubs = m.markdownEditorStubs[1:len(m.markdownEditorStubs)] + if s.prompt != prompt { + return "", NoSuchPromptErr(prompt) + } + return s.fn(prompt, defaultValue, blankAllowed) +} + +func (m *MockPrompter) RegisterAuthToken(stub func() (string, error)) { + m.authTokenStubs = append(m.authTokenStubs, authTokenStub{fn: stub}) +} + +func (m *MockPrompter) RegisterConfirmDeletion(prompt string, stub func(string) error) { + m.confirmDeletionStubs = append(m.confirmDeletionStubs, confirmDeletionStub{prompt: prompt, fn: stub}) +} + +func (m *MockPrompter) RegisterInputHostname(stub func() (string, error)) { + m.inputHostnameStubs = append(m.inputHostnameStubs, inputHostnameStub{fn: stub}) +} + +func (m *MockPrompter) RegisterMarkdownEditor(prompt string, stub func(string, string, bool) (string, error)) { + m.markdownEditorStubs = append(m.markdownEditorStubs, markdownEditorStub{prompt: prompt, fn: stub}) +} + +func (m *MockPrompter) Verify() { + errs := []string{} + if len(m.authTokenStubs) > 0 { + errs = append(errs, "AuthToken") + } + if len(m.confirmDeletionStubs) > 0 { + errs = append(errs, "ConfirmDeletion") + } + if len(m.inputHostnameStubs) > 0 { + errs = append(errs, "inputHostname") + } + if len(m.markdownEditorStubs) > 0 { + errs = append(errs, "markdownEditorStubs") + } + if len(errs) > 0 { + m.t.Helper() + m.t.Errorf("%d unmatched calls to %s", len(errs), strings.Join(errs, ",")) + } +} + +func AssertOptions(t *testing.T, expected, actual []string) { + assert.Equal(t, expected, actual) +} + +func IndexFor(options []string, answer string) (int, error) { + for ix, a := range options { + if a == answer { + return ix, nil + } + } + return -1, NoSuchAnswerErr(answer, options) +} + +func NoSuchPromptErr(prompt string) error { + return fmt.Errorf("no such prompt '%s'", prompt) +} + +func NoSuchAnswerErr(answer string, options []string) error { + return fmt.Errorf("no such answer '%s' in [%s]", answer, strings.Join(options, ", ")) +} diff --git a/vendor/github.com/cli/cli/v2/internal/run/run.go b/vendor/github.com/cli/cli/v2/internal/run/run.go new file mode 100644 index 000000000..3a166e7ba --- /dev/null +++ b/vendor/github.com/cli/cli/v2/internal/run/run.go @@ -0,0 +1,98 @@ +package run + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/cli/cli/v2/utils" +) + +// Runnable is typically an exec.Cmd or its stub in tests +type Runnable interface { + Output() ([]byte, error) + Run() error +} + +// PrepareCmd extends exec.Cmd with extra error reporting features and provides a +// hook to stub command execution in tests +var PrepareCmd = func(cmd *exec.Cmd) Runnable { + return &cmdWithStderr{cmd} +} + +// cmdWithStderr augments exec.Cmd by adding stderr to the error message +type cmdWithStderr struct { + *exec.Cmd +} + +func (c cmdWithStderr) Output() ([]byte, error) { + if isVerbose, _ := utils.IsDebugEnabled(); isVerbose { + _ = printArgs(os.Stderr, c.Cmd.Args) + } + out, err := c.Cmd.Output() + if c.Cmd.Stderr != nil || err == nil { + return out, err + } + cmdErr := &CmdError{ + Args: c.Cmd.Args, + Err: err, + } + var exitError *exec.ExitError + if errors.As(err, &exitError) { + cmdErr.Stderr = bytes.NewBuffer(exitError.Stderr) + } + return out, cmdErr +} + +func (c cmdWithStderr) Run() error { + if isVerbose, _ := utils.IsDebugEnabled(); isVerbose { + _ = printArgs(os.Stderr, c.Cmd.Args) + } + if c.Cmd.Stderr != nil { + return c.Cmd.Run() + } + errStream := &bytes.Buffer{} + c.Cmd.Stderr = errStream + err := c.Cmd.Run() + if err != nil { + err = &CmdError{ + Args: c.Cmd.Args, + Err: err, + Stderr: errStream, + } + } + return err +} + +// CmdError provides more visibility into why an exec.Cmd had failed +type CmdError struct { + Args []string + Err error + Stderr *bytes.Buffer +} + +func (e CmdError) Error() string { + msg := e.Stderr.String() + if msg != "" && !strings.HasSuffix(msg, "\n") { + msg += "\n" + } + return fmt.Sprintf("%s%s: %s", msg, e.Args[0], e.Err) +} + +func (e CmdError) Unwrap() error { + return e.Err +} + +func printArgs(w io.Writer, args []string) error { + if len(args) > 0 { + // print commands, but omit the full path to an executable + args = append([]string{filepath.Base(args[0])}, args[1:]...) + } + _, err := fmt.Fprintf(w, "%v\n", args) + return err +} diff --git a/vendor/github.com/cli/cli/v2/internal/run/stub.go b/vendor/github.com/cli/cli/v2/internal/run/stub.go new file mode 100644 index 000000000..49bf62d29 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/internal/run/stub.go @@ -0,0 +1,151 @@ +package run + +import ( + "fmt" + "os/exec" + "path/filepath" + "regexp" + "strings" +) + +const ( + gitAuthRE = `-c credential.helper= -c credential.helper=!"[^"]+" auth git-credential ` +) + +type T interface { + Helper() + Errorf(string, ...interface{}) +} + +// Stub installs a catch-all for all external commands invoked from gh. It returns a restore func that, when +// invoked from tests, fails the current test if some stubs that were registered were never matched. +func Stub() (*CommandStubber, func(T)) { + cs := &CommandStubber{} + teardown := setPrepareCmd(func(cmd *exec.Cmd) Runnable { + s := cs.find(cmd.Args) + if s == nil { + panic(fmt.Sprintf("no exec stub for `%s`", strings.Join(cmd.Args, " "))) + } + for _, c := range s.callbacks { + c(cmd.Args) + } + s.matched = true + return s + }) + + return cs, func(t T) { + defer teardown() + var unmatched []string + for _, s := range cs.stubs { + if s.matched { + continue + } + unmatched = append(unmatched, s.pattern.String()) + } + if len(unmatched) == 0 { + return + } + t.Helper() + t.Errorf("unmatched stubs (%d): %s", len(unmatched), strings.Join(unmatched, ", ")) + } +} + +func setPrepareCmd(fn func(*exec.Cmd) Runnable) func() { + origPrepare := PrepareCmd + PrepareCmd = func(cmd *exec.Cmd) Runnable { + // normalize git executable name for consistency in tests + if baseName := filepath.Base(cmd.Args[0]); baseName == "git" || baseName == "git.exe" { + cmd.Args[0] = "git" + } + return fn(cmd) + } + return func() { + PrepareCmd = origPrepare + } +} + +// CommandStubber stubs out invocations to external commands. +type CommandStubber struct { + stubs []*commandStub +} + +// Register a stub for an external command. Pattern is a regular expression, output is the standard output +// from a command. Pass callbacks to inspect raw arguments that the command was invoked with. +func (cs *CommandStubber) Register(pattern string, exitStatus int, output string, callbacks ...CommandCallback) { + if len(pattern) < 1 { + panic("cannot use empty regexp pattern") + } + if strings.HasPrefix(pattern, "git") { + pattern = addGitAuthentication(pattern) + } + cs.stubs = append(cs.stubs, &commandStub{ + pattern: regexp.MustCompile(pattern), + exitStatus: exitStatus, + stdout: output, + callbacks: callbacks, + }) +} + +func (cs *CommandStubber) find(args []string) *commandStub { + line := strings.Join(args, " ") + for _, s := range cs.stubs { + if !s.matched && s.pattern.MatchString(line) { + return s + } + } + return nil +} + +type CommandCallback func([]string) + +type commandStub struct { + pattern *regexp.Regexp + matched bool + exitStatus int + stdout string + callbacks []CommandCallback +} + +type errWithExitCode struct { + message string + exitCode int +} + +func (e errWithExitCode) Error() string { + return e.message +} + +func (e errWithExitCode) ExitCode() int { + return e.exitCode +} + +// Run satisfies Runnable +func (s *commandStub) Run() error { + if s.exitStatus != 0 { + // It's nontrivial to construct a fake `exec.ExitError` instance, so we return an error type + // that has the `ExitCode() int` method. + return errWithExitCode{ + message: fmt.Sprintf("%s exited with status %d", s.pattern, s.exitStatus), + exitCode: s.exitStatus, + } + } + return nil +} + +// Output satisfies Runnable +func (s *commandStub) Output() ([]byte, error) { + if err := s.Run(); err != nil { + return []byte(nil), err + } + return []byte(s.stdout), nil +} + +// Inject git authentication string for specific git commands. +func addGitAuthentication(s string) string { + pattern := regexp.MustCompile(`( fetch | pull | push | clone | remote add.+-f | submodule )`) + loc := pattern.FindStringIndex(s) + if loc == nil { + return s + } + return s[:loc[0]+1] + gitAuthRE + s[loc[0]+1:] +} diff --git a/vendor/github.com/cli/cli/v2/pkg/cmd/auth/shared/git_credential.go b/vendor/github.com/cli/cli/v2/pkg/cmd/auth/shared/git_credential.go new file mode 100644 index 000000000..8624cf00b --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/cmd/auth/shared/git_credential.go @@ -0,0 +1,187 @@ +package shared + +import ( + "bytes" + "context" + "errors" + "fmt" + "path/filepath" + "strings" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/git" + "github.com/cli/cli/v2/internal/ghinstance" + "github.com/google/shlex" +) + +type GitCredentialFlow struct { + Executable string + Prompter Prompt + GitClient *git.Client + + shouldSetup bool + helper string + scopes []string +} + +func (flow *GitCredentialFlow) Prompt(hostname string) error { + var gitErr error + flow.helper, gitErr = gitCredentialHelper(flow.GitClient, hostname) + if isOurCredentialHelper(flow.helper) { + flow.scopes = append(flow.scopes, "workflow") + return nil + } + + result, err := flow.Prompter.Confirm("Authenticate Git with your GitHub credentials?", true) + if err != nil { + return err + } + flow.shouldSetup = result + + if flow.shouldSetup { + if isGitMissing(gitErr) { + return gitErr + } + flow.scopes = append(flow.scopes, "workflow") + } + + return nil +} + +func (flow *GitCredentialFlow) Scopes() []string { + return flow.scopes +} + +func (flow *GitCredentialFlow) ShouldSetup() bool { + return flow.shouldSetup +} + +func (flow *GitCredentialFlow) Setup(hostname, username, authToken string) error { + return flow.gitCredentialSetup(hostname, username, authToken) +} + +func (flow *GitCredentialFlow) gitCredentialSetup(hostname, username, password string) error { + gitClient := flow.GitClient + ctx := context.Background() + + if flow.helper == "" { + credHelperKeys := []string{ + gitCredentialHelperKey(hostname), + } + + gistHost := strings.TrimSuffix(ghinstance.GistHost(hostname), "/") + if strings.HasPrefix(gistHost, "gist.") { + credHelperKeys = append(credHelperKeys, gitCredentialHelperKey(gistHost)) + } + + var configErr error + + for _, credHelperKey := range credHelperKeys { + if configErr != nil { + break + } + // first use a blank value to indicate to git we want to sever the chain of credential helpers + preConfigureCmd, err := gitClient.Command(ctx, "config", "--global", "--replace-all", credHelperKey, "") + if err != nil { + configErr = err + break + } + if _, err = preConfigureCmd.Output(); err != nil { + configErr = err + break + } + + // second configure the actual helper for this host + configureCmd, err := gitClient.Command(ctx, + "config", "--global", "--add", + credHelperKey, + fmt.Sprintf("!%s auth git-credential", shellQuote(flow.Executable)), + ) + if err != nil { + configErr = err + } else { + _, configErr = configureCmd.Output() + } + } + + return configErr + } + + // clear previous cached credentials + rejectCmd, err := gitClient.Command(ctx, "credential", "reject") + if err != nil { + return err + } + + rejectCmd.Stdin = bytes.NewBufferString(heredoc.Docf(` + protocol=https + host=%s + `, hostname)) + + _, err = rejectCmd.Output() + if err != nil { + return err + } + + approveCmd, err := gitClient.Command(ctx, "credential", "approve") + if err != nil { + return err + } + + approveCmd.Stdin = bytes.NewBufferString(heredoc.Docf(` + protocol=https + host=%s + username=%s + password=%s + `, hostname, username, password)) + + _, err = approveCmd.Output() + if err != nil { + return err + } + + return nil +} + +func gitCredentialHelperKey(hostname string) string { + host := strings.TrimSuffix(ghinstance.HostPrefix(hostname), "/") + return fmt.Sprintf("credential.%s.helper", host) +} + +func gitCredentialHelper(gitClient *git.Client, hostname string) (helper string, err error) { + ctx := context.Background() + helper, err = gitClient.Config(ctx, gitCredentialHelperKey(hostname)) + if helper != "" { + return + } + helper, err = gitClient.Config(ctx, "credential.helper") + return +} + +func isOurCredentialHelper(cmd string) bool { + if !strings.HasPrefix(cmd, "!") { + return false + } + + args, err := shlex.Split(cmd[1:]) + if err != nil || len(args) == 0 { + return false + } + + return strings.TrimSuffix(filepath.Base(args[0]), ".exe") == "gh" +} + +func isGitMissing(err error) bool { + if err == nil { + return false + } + var errNotInstalled *git.NotInstalled + return errors.As(err, &errNotInstalled) +} + +func shellQuote(s string) string { + if strings.ContainsAny(s, " $\\") { + return "'" + s + "'" + } + return s +} diff --git a/vendor/github.com/cli/cli/v2/pkg/cmd/auth/shared/login_flow.go b/vendor/github.com/cli/cli/v2/pkg/cmd/auth/shared/login_flow.go new file mode 100644 index 000000000..7bb974547 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/cmd/auth/shared/login_flow.go @@ -0,0 +1,285 @@ +package shared + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "os" + "slices" + "strings" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/git" + "github.com/cli/cli/v2/internal/authflow" + "github.com/cli/cli/v2/internal/browser" + "github.com/cli/cli/v2/internal/ghinstance" + "github.com/cli/cli/v2/pkg/cmd/ssh-key/add" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/ssh" +) + +const defaultSSHKeyTitle = "GitHub CLI" + +type iconfig interface { + Login(string, string, string, string, bool) (bool, error) + UsersForHost(string) []string +} + +type LoginOptions struct { + IO *iostreams.IOStreams + Config iconfig + HTTPClient *http.Client + GitClient *git.Client + Hostname string + Interactive bool + Web bool + Scopes []string + Executable string + GitProtocol string + Prompter Prompt + Browser browser.Browser + SecureStorage bool + + sshContext ssh.Context +} + +func Login(opts *LoginOptions) error { + cfg := opts.Config + hostname := opts.Hostname + httpClient := opts.HTTPClient + cs := opts.IO.ColorScheme() + + gitProtocol := strings.ToLower(opts.GitProtocol) + if opts.Interactive && gitProtocol == "" { + options := []string{ + "HTTPS", + "SSH", + } + result, err := opts.Prompter.Select( + "What is your preferred protocol for Git operations on this host?", + options[0], + options) + if err != nil { + return err + } + proto := options[result] + gitProtocol = strings.ToLower(proto) + } + + var additionalScopes []string + + credentialFlow := &GitCredentialFlow{ + Executable: opts.Executable, + Prompter: opts.Prompter, + GitClient: opts.GitClient, + } + if opts.Interactive && gitProtocol == "https" { + if err := credentialFlow.Prompt(hostname); err != nil { + return err + } + additionalScopes = append(additionalScopes, credentialFlow.Scopes()...) + } + + var keyToUpload string + keyTitle := defaultSSHKeyTitle + if opts.Interactive && gitProtocol == "ssh" { + pubKeys, err := opts.sshContext.LocalPublicKeys() + if err != nil { + return err + } + + if len(pubKeys) > 0 { + options := append(pubKeys, "Skip") + keyChoice, err := opts.Prompter.Select( + "Upload your SSH public key to your GitHub account?", + options[0], + options) + if err != nil { + return err + } + if keyChoice < len(pubKeys) { + keyToUpload = pubKeys[keyChoice] + } + } else if opts.sshContext.HasKeygen() { + sshChoice, err := opts.Prompter.Confirm("Generate a new SSH key to add to your GitHub account?", true) + if err != nil { + return err + } + + if sshChoice { + passphrase, err := opts.Prompter.Password( + "Enter a passphrase for your new SSH key (Optional)") + if err != nil { + return err + } + keyPair, err := opts.sshContext.GenerateSSHKey("id_ed25519", passphrase) + if err != nil { + return err + } + keyToUpload = keyPair.PublicKeyPath + } + } + + if keyToUpload != "" { + var err error + keyTitle, err = opts.Prompter.Input( + "Title for your SSH key:", defaultSSHKeyTitle) + if err != nil { + return err + } + + additionalScopes = append(additionalScopes, "admin:public_key") + } + } + + var authMode int + if opts.Web { + authMode = 0 + } else if opts.Interactive { + options := []string{"Login with a web browser", "Paste an authentication token"} + var err error + authMode, err = opts.Prompter.Select( + "How would you like to authenticate GitHub CLI?", + options[0], + options) + if err != nil { + return err + } + } + + var authToken string + var username string + + if authMode == 0 { + var err error + authToken, username, err = authflow.AuthFlow(hostname, opts.IO, "", append(opts.Scopes, additionalScopes...), opts.Interactive, opts.Browser) + if err != nil { + return fmt.Errorf("failed to authenticate via web browser: %w", err) + } + fmt.Fprintf(opts.IO.ErrOut, "%s Authentication complete.\n", cs.SuccessIcon()) + } else { + minimumScopes := append([]string{"repo", "read:org"}, additionalScopes...) + fmt.Fprint(opts.IO.ErrOut, heredoc.Docf(` + Tip: you can generate a Personal Access Token here https://%s/settings/tokens + The minimum required scopes are %s. + `, hostname, scopesSentence(minimumScopes))) + + var err error + authToken, err = opts.Prompter.AuthToken() + if err != nil { + return err + } + + if err := HasMinimumScopes(httpClient, hostname, authToken); err != nil { + return fmt.Errorf("error validating token: %w", err) + } + } + + if username == "" { + var err error + username, err = GetCurrentLogin(httpClient, hostname, authToken) + if err != nil { + return fmt.Errorf("error retrieving current user: %w", err) + } + } + + // Get these users before adding the new one, so that we can + // check whether the user was already logged in later. + // + // In this case we ignore the error if the host doesn't exist + // because that can occur when the user is logging into a host + // for the first time. + usersForHost := cfg.UsersForHost(hostname) + userWasAlreadyLoggedIn := slices.Contains(usersForHost, username) + + if gitProtocol != "" { + fmt.Fprintf(opts.IO.ErrOut, "- gh config set -h %s git_protocol %s\n", hostname, gitProtocol) + fmt.Fprintf(opts.IO.ErrOut, "%s Configured git protocol\n", cs.SuccessIcon()) + } + + insecureStorageUsed, err := cfg.Login(hostname, username, authToken, gitProtocol, opts.SecureStorage) + if err != nil { + return err + } + if insecureStorageUsed { + fmt.Fprintf(opts.IO.ErrOut, "%s Authentication credentials saved in plain text\n", cs.Yellow("!")) + } + + if credentialFlow.ShouldSetup() { + err := credentialFlow.Setup(hostname, username, authToken) + if err != nil { + return err + } + } + + if keyToUpload != "" { + uploaded, err := sshKeyUpload(httpClient, hostname, keyToUpload, keyTitle) + if err != nil { + return err + } + + if uploaded { + fmt.Fprintf(opts.IO.ErrOut, "%s Uploaded the SSH key to your GitHub account: %s\n", cs.SuccessIcon(), cs.Bold(keyToUpload)) + } else { + fmt.Fprintf(opts.IO.ErrOut, "%s SSH key already existed on your GitHub account: %s\n", cs.SuccessIcon(), cs.Bold(keyToUpload)) + } + } + + fmt.Fprintf(opts.IO.ErrOut, "%s Logged in as %s\n", cs.SuccessIcon(), cs.Bold(username)) + if userWasAlreadyLoggedIn { + fmt.Fprintf(opts.IO.ErrOut, "%s You were already logged in to this account\n", cs.WarningIcon()) + } + + return nil +} + +func scopesSentence(scopes []string) string { + quoted := make([]string, len(scopes)) + for i, s := range scopes { + quoted[i] = fmt.Sprintf("'%s'", s) + } + return strings.Join(quoted, ", ") +} + +func sshKeyUpload(httpClient *http.Client, hostname, keyFile string, title string) (bool, error) { + f, err := os.Open(keyFile) + if err != nil { + return false, err + } + defer f.Close() + + return add.SSHKeyUpload(httpClient, hostname, f, title) +} + +func GetCurrentLogin(httpClient httpClient, hostname, authToken string) (string, error) { + query := `query UserCurrent{viewer{login}}` + reqBody, err := json.Marshal(map[string]interface{}{"query": query}) + if err != nil { + return "", err + } + result := struct { + Data struct{ Viewer struct{ Login string } } + }{} + apiEndpoint := ghinstance.GraphQLEndpoint(hostname) + req, err := http.NewRequest("POST", apiEndpoint, bytes.NewBuffer(reqBody)) + if err != nil { + return "", err + } + req.Header.Set("Authorization", "token "+authToken) + res, err := httpClient.Do(req) + if err != nil { + return "", err + } + defer res.Body.Close() + if res.StatusCode > 299 { + return "", api.HandleHTTPError(res) + } + decoder := json.NewDecoder(res.Body) + err = decoder.Decode(&result) + if err != nil { + return "", err + } + return result.Data.Viewer.Login, nil +} diff --git a/vendor/github.com/cli/cli/v2/pkg/cmd/auth/shared/oauth_scopes.go b/vendor/github.com/cli/cli/v2/pkg/cmd/auth/shared/oauth_scopes.go new file mode 100644 index 000000000..8d9996019 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/cmd/auth/shared/oauth_scopes.go @@ -0,0 +1,106 @@ +package shared + +import ( + "fmt" + "io" + "net/http" + "strings" + + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/ghinstance" +) + +type MissingScopesError struct { + MissingScopes []string +} + +func (e MissingScopesError) Error() string { + var missing []string + for _, s := range e.MissingScopes { + missing = append(missing, fmt.Sprintf("'%s'", s)) + } + scopes := strings.Join(missing, ", ") + + if len(e.MissingScopes) == 1 { + return "missing required scope " + scopes + } + return "missing required scopes " + scopes +} + +type httpClient interface { + Do(*http.Request) (*http.Response, error) +} + +// GetScopes performs a GitHub API request and returns the value of the X-Oauth-Scopes header. +func GetScopes(httpClient httpClient, hostname, authToken string) (string, error) { + apiEndpoint := ghinstance.RESTPrefix(hostname) + + req, err := http.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return "", err + } + + req.Header.Set("Authorization", "token "+authToken) + + res, err := httpClient.Do(req) + if err != nil { + return "", err + } + + defer func() { + // Ensure the response body is fully read and closed + // before we reconnect, so that we reuse the same TCPconnection. + _, _ = io.Copy(io.Discard, res.Body) + res.Body.Close() + }() + + if res.StatusCode != 200 { + return "", api.HandleHTTPError(res) + } + + return res.Header.Get("X-Oauth-Scopes"), nil +} + +// HasMinimumScopes performs a GitHub API request and returns an error if the token used in the request +// lacks the minimum required scopes for performing API operations with gh. +func HasMinimumScopes(httpClient httpClient, hostname, authToken string) error { + scopesHeader, err := GetScopes(httpClient, hostname, authToken) + if err != nil { + return err + } + + return HeaderHasMinimumScopes(scopesHeader) +} + +// HeaderHasMinimumScopes parses the comma separated scopesHeader string and returns an error +// if it lacks the minimum required scopes for performing API operations with gh. +func HeaderHasMinimumScopes(scopesHeader string) error { + if scopesHeader == "" { + // if the token reports no scopes, assume that it's an integration token and give up on + // detecting its capabilities + return nil + } + + search := map[string]bool{ + "repo": false, + "read:org": false, + "admin:org": false, + } + for _, s := range strings.Split(scopesHeader, ",") { + search[strings.TrimSpace(s)] = true + } + + var missingScopes []string + if !search["repo"] { + missingScopes = append(missingScopes, "repo") + } + + if !search["read:org"] && !search["write:org"] && !search["admin:org"] { + missingScopes = append(missingScopes, "read:org") + } + + if len(missingScopes) > 0 { + return &MissingScopesError{MissingScopes: missingScopes} + } + return nil +} diff --git a/vendor/github.com/cli/cli/v2/pkg/cmd/auth/shared/prompt.go b/vendor/github.com/cli/cli/v2/pkg/cmd/auth/shared/prompt.go new file mode 100644 index 000000000..c0d47372c --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/cmd/auth/shared/prompt.go @@ -0,0 +1,10 @@ +package shared + +type Prompt interface { + Select(string, string, []string) (int, error) + Confirm(string, bool) (bool, error) + InputHostname() (string, error) + AuthToken() (string, error) + Input(string, string) (string, error) + Password(string) (string, error) +} diff --git a/vendor/github.com/cli/cli/v2/pkg/cmd/auth/shared/writeable.go b/vendor/github.com/cli/cli/v2/pkg/cmd/auth/shared/writeable.go new file mode 100644 index 000000000..e5ae91469 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/cmd/auth/shared/writeable.go @@ -0,0 +1,12 @@ +package shared + +import ( + "strings" + + "github.com/cli/cli/v2/internal/config" +) + +func AuthTokenWriteable(authCfg *config.AuthConfig, hostname string) (string, bool) { + token, src := authCfg.ActiveToken(hostname) + return src, (token == "" || !strings.HasSuffix(src, "_TOKEN")) +} diff --git a/vendor/github.com/cli/cli/v2/pkg/cmd/ssh-key/add/add.go b/vendor/github.com/cli/cli/v2/pkg/cmd/ssh-key/add/add.go new file mode 100644 index 000000000..553c35985 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/cmd/ssh-key/add/add.go @@ -0,0 +1,107 @@ +package add + +import ( + "fmt" + "io" + "net/http" + "os" + + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/pkg/cmd/ssh-key/shared" + "github.com/cli/cli/v2/pkg/cmdutil" + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/spf13/cobra" +) + +type AddOptions struct { + IO *iostreams.IOStreams + Config func() (config.Config, error) + HTTPClient func() (*http.Client, error) + + KeyFile string + Title string + Type string +} + +func NewCmdAdd(f *cmdutil.Factory, runF func(*AddOptions) error) *cobra.Command { + opts := &AddOptions{ + HTTPClient: f.HttpClient, + Config: f.Config, + IO: f.IOStreams, + } + + cmd := &cobra.Command{ + Use: "add []", + Short: "Add an SSH key to your GitHub account", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + if opts.IO.IsStdoutTTY() && opts.IO.IsStdinTTY() { + return cmdutil.FlagErrorf("public key file missing") + } + opts.KeyFile = "-" + } else { + opts.KeyFile = args[0] + } + + if runF != nil { + return runF(opts) + } + return runAdd(opts) + }, + } + + typeEnums := []string{shared.AuthenticationKey, shared.SigningKey} + cmdutil.StringEnumFlag(cmd, &opts.Type, "type", "", shared.AuthenticationKey, typeEnums, "Type of the ssh key") + cmd.Flags().StringVarP(&opts.Title, "title", "t", "", "Title for the new key") + return cmd +} + +func runAdd(opts *AddOptions) error { + httpClient, err := opts.HTTPClient() + if err != nil { + return err + } + + var keyReader io.Reader + if opts.KeyFile == "-" { + keyReader = opts.IO.In + defer opts.IO.In.Close() + } else { + f, err := os.Open(opts.KeyFile) + if err != nil { + return err + } + defer f.Close() + keyReader = f + } + + cfg, err := opts.Config() + if err != nil { + return err + } + + hostname, _ := cfg.Authentication().DefaultHost() + + var uploaded bool + + if opts.Type == shared.SigningKey { + uploaded, err = SSHSigningKeyUpload(httpClient, hostname, keyReader, opts.Title) + } else { + uploaded, err = SSHKeyUpload(httpClient, hostname, keyReader, opts.Title) + } + + if err != nil { + return err + } + + cs := opts.IO.ColorScheme() + + if uploaded { + fmt.Fprintf(opts.IO.ErrOut, "%s Public key added to your account\n", cs.SuccessIcon()) + } else { + fmt.Fprintf(opts.IO.ErrOut, "%s Public key already exists on your account\n", cs.SuccessIcon()) + } + + return nil +} diff --git a/vendor/github.com/cli/cli/v2/pkg/cmd/ssh-key/add/http.go b/vendor/github.com/cli/cli/v2/pkg/cmd/ssh-key/add/http.go new file mode 100644 index 000000000..83aa77bdc --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/cmd/ssh-key/add/http.go @@ -0,0 +1,127 @@ +package add + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "net/http" + "strings" + + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/ghinstance" + "github.com/cli/cli/v2/pkg/cmd/ssh-key/shared" +) + +// Uploads the provided SSH key. Returns true if the key was uploaded, false if it was not. +func SSHKeyUpload(httpClient *http.Client, hostname string, keyFile io.Reader, title string) (bool, error) { + url := ghinstance.RESTPrefix(hostname) + "user/keys" + + keyBytes, err := io.ReadAll(keyFile) + if err != nil { + return false, err + } + + fullUserKey := string(keyBytes) + splitKey := strings.Fields(fullUserKey) + if len(splitKey) < 2 { + return false, errors.New("provided key is not in a valid format") + } + + keyToCompare := splitKey[0] + " " + splitKey[1] + + keys, err := shared.UserKeys(httpClient, hostname, "") + if err != nil { + return false, err + } + + for _, k := range keys { + if k.Key == keyToCompare { + return false, nil + } + } + + payload := map[string]string{ + "title": title, + "key": fullUserKey, + } + + err = keyUpload(httpClient, url, payload) + + if err != nil { + return false, err + } + + return true, nil +} + +// Uploads the provided SSH Signing key. Returns true if the key was uploaded, false if it was not. +func SSHSigningKeyUpload(httpClient *http.Client, hostname string, keyFile io.Reader, title string) (bool, error) { + url := ghinstance.RESTPrefix(hostname) + "user/ssh_signing_keys" + + keyBytes, err := io.ReadAll(keyFile) + if err != nil { + return false, err + } + + fullUserKey := string(keyBytes) + splitKey := strings.Fields(fullUserKey) + if len(splitKey) < 2 { + return false, errors.New("provided key is not in a valid format") + } + + keyToCompare := splitKey[0] + " " + splitKey[1] + + keys, err := shared.UserSigningKeys(httpClient, hostname, "") + if err != nil { + return false, err + } + + for _, k := range keys { + if k.Key == keyToCompare { + return false, nil + } + } + + payload := map[string]string{ + "title": title, + "key": fullUserKey, + } + + err = keyUpload(httpClient, url, payload) + + if err != nil { + return false, err + } + + return true, nil +} + +func keyUpload(httpClient *http.Client, url string, payload map[string]string) error { + payloadBytes, err := json.Marshal(payload) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(payloadBytes)) + if err != nil { + return err + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode > 299 { + return api.HandleHTTPError(resp) + } + + _, err = io.Copy(io.Discard, resp.Body) + if err != nil { + return err + } + + return nil +} diff --git a/vendor/github.com/cli/cli/v2/pkg/cmd/ssh-key/shared/user_keys.go b/vendor/github.com/cli/cli/v2/pkg/cmd/ssh-key/shared/user_keys.go new file mode 100644 index 000000000..035d00244 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/cmd/ssh-key/shared/user_keys.go @@ -0,0 +1,95 @@ +package shared + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/cli/cli/v2/api" + "github.com/cli/cli/v2/internal/ghinstance" +) + +const ( + AuthenticationKey = "authentication" + SigningKey = "signing" +) + +type sshKey struct { + ID int + Key string + Title string + Type string + CreatedAt time.Time `json:"created_at"` +} + +func UserKeys(httpClient *http.Client, host, userHandle string) ([]sshKey, error) { + resource := "user/keys" + if userHandle != "" { + resource = fmt.Sprintf("users/%s/keys", userHandle) + } + url := fmt.Sprintf("%s%s?per_page=%d", ghinstance.RESTPrefix(host), resource, 100) + + keys, err := getUserKeys(httpClient, url) + + if err != nil { + return nil, err + } + + for i := 0; i < len(keys); i++ { + keys[i].Type = AuthenticationKey + } + + return keys, nil +} + +func UserSigningKeys(httpClient *http.Client, host, userHandle string) ([]sshKey, error) { + resource := "user/ssh_signing_keys" + if userHandle != "" { + resource = fmt.Sprintf("users/%s/ssh_signing_keys", userHandle) + } + url := fmt.Sprintf("%s%s?per_page=%d", ghinstance.RESTPrefix(host), resource, 100) + + keys, err := getUserKeys(httpClient, url) + + if err != nil { + return nil, err + } + + for i := 0; i < len(keys); i++ { + keys[i].Type = SigningKey + } + + return keys, nil +} + +func getUserKeys(httpClient *http.Client, url string) ([]sshKey, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + resp, err := httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode > 299 { + return nil, api.HandleHTTPError(resp) + } + + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var keys []sshKey + err = json.Unmarshal(b, &keys) + if err != nil { + return nil, err + } + + return keys, nil +} diff --git a/vendor/github.com/cli/cli/v2/pkg/cmdutil/args.go b/vendor/github.com/cli/cli/v2/pkg/cmdutil/args.go new file mode 100644 index 000000000..0f03a07bd --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/cmdutil/args.go @@ -0,0 +1,59 @@ +package cmdutil + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +func MinimumArgs(n int, msg string) cobra.PositionalArgs { + if msg == "" { + return cobra.MinimumNArgs(1) + } + + return func(cmd *cobra.Command, args []string) error { + if len(args) < n { + return FlagErrorf("%s", msg) + } + return nil + } +} + +func ExactArgs(n int, msg string) cobra.PositionalArgs { + return func(cmd *cobra.Command, args []string) error { + if len(args) > n { + return FlagErrorf("too many arguments") + } + + if len(args) < n { + return FlagErrorf("%s", msg) + } + + return nil + } +} + +func NoArgsQuoteReminder(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return nil + } + + errMsg := fmt.Sprintf("unknown argument %q", args[0]) + if len(args) > 1 { + errMsg = fmt.Sprintf("unknown arguments %q", args) + } + + hasValueFlag := false + cmd.Flags().Visit(func(f *pflag.Flag) { + if f.Value.Type() != "bool" { + hasValueFlag = true + } + }) + + if hasValueFlag { + errMsg += "; please quote all values that have spaces" + } + + return FlagErrorf("%s", errMsg) +} diff --git a/vendor/github.com/cli/cli/v2/pkg/cmdutil/auth_check.go b/vendor/github.com/cli/cli/v2/pkg/cmdutil/auth_check.go new file mode 100644 index 000000000..56ebb0c4d --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/cmdutil/auth_check.go @@ -0,0 +1,41 @@ +package cmdutil + +import ( + "github.com/cli/cli/v2/internal/config" + "github.com/spf13/cobra" +) + +func DisableAuthCheck(cmd *cobra.Command) { + if cmd.Annotations == nil { + cmd.Annotations = map[string]string{} + } + + cmd.Annotations["skipAuthCheck"] = "true" +} + +func CheckAuth(cfg config.Config) bool { + if cfg.Authentication().HasEnvToken() { + return true + } + + if len(cfg.Authentication().Hosts()) > 0 { + return true + } + + return false +} + +func IsAuthCheckEnabled(cmd *cobra.Command) bool { + switch cmd.Name() { + case "help", cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd: + return false + } + + for c := cmd; c.Parent() != nil; c = c.Parent() { + if c.Annotations != nil && c.Annotations["skipAuthCheck"] == "true" { + return false + } + } + + return true +} diff --git a/vendor/github.com/cli/cli/v2/pkg/cmdutil/cmdgroup.go b/vendor/github.com/cli/cli/v2/pkg/cmdutil/cmdgroup.go new file mode 100644 index 000000000..e1b7b2362 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/cmdutil/cmdgroup.go @@ -0,0 +1,15 @@ +package cmdutil + +import "github.com/spf13/cobra" + +func AddGroup(parent *cobra.Command, title string, cmds ...*cobra.Command) { + g := &cobra.Group{ + Title: title, + ID: title, + } + parent.AddGroup(g) + for _, c := range cmds { + c.GroupID = g.ID + parent.AddCommand(c) + } +} diff --git a/vendor/github.com/cli/cli/v2/pkg/cmdutil/errors.go b/vendor/github.com/cli/cli/v2/pkg/cmdutil/errors.go new file mode 100644 index 000000000..26a059d25 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/cmdutil/errors.go @@ -0,0 +1,70 @@ +package cmdutil + +import ( + "errors" + "fmt" + + "github.com/AlecAivazis/survey/v2/terminal" +) + +// FlagErrorf returns a new FlagError that wraps an error produced by +// fmt.Errorf(format, args...). +func FlagErrorf(format string, args ...interface{}) error { + return FlagErrorWrap(fmt.Errorf(format, args...)) +} + +// FlagError returns a new FlagError that wraps the specified error. +func FlagErrorWrap(err error) error { return &FlagError{err} } + +// A *FlagError indicates an error processing command-line flags or other arguments. +// Such errors cause the application to display the usage message. +type FlagError struct { + // Note: not struct{error}: only *FlagError should satisfy error. + err error +} + +func (fe *FlagError) Error() string { + return fe.err.Error() +} + +func (fe *FlagError) Unwrap() error { + return fe.err +} + +// SilentError is an error that triggers exit code 1 without any error messaging +var SilentError = errors.New("SilentError") + +// CancelError signals user-initiated cancellation +var CancelError = errors.New("CancelError") + +// PendingError signals nothing failed but something is pending +var PendingError = errors.New("PendingError") + +func IsUserCancellation(err error) bool { + return errors.Is(err, CancelError) || errors.Is(err, terminal.InterruptErr) +} + +func MutuallyExclusive(message string, conditions ...bool) error { + numTrue := 0 + for _, ok := range conditions { + if ok { + numTrue++ + } + } + if numTrue > 1 { + return FlagErrorf("%s", message) + } + return nil +} + +type NoResultsError struct { + message string +} + +func (e NoResultsError) Error() string { + return e.message +} + +func NewNoResultsError(message string) NoResultsError { + return NoResultsError{message: message} +} diff --git a/vendor/github.com/cli/cli/v2/pkg/cmdutil/factory.go b/vendor/github.com/cli/cli/v2/pkg/cmdutil/factory.go new file mode 100644 index 000000000..b48dceed3 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/cmdutil/factory.go @@ -0,0 +1,100 @@ +package cmdutil + +import ( + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/cli/cli/v2/context" + "github.com/cli/cli/v2/git" + "github.com/cli/cli/v2/internal/browser" + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/internal/prompter" + "github.com/cli/cli/v2/pkg/extensions" + "github.com/cli/cli/v2/pkg/iostreams" +) + +type Factory struct { + AppVersion string + ExecutableName string + + Browser browser.Browser + ExtensionManager extensions.ExtensionManager + GitClient *git.Client + IOStreams *iostreams.IOStreams + Prompter prompter.Prompter + + BaseRepo func() (ghrepo.Interface, error) + Branch func() (string, error) + Config func() (config.Config, error) + HttpClient func() (*http.Client, error) + Remotes func() (context.Remotes, error) +} + +// Executable is the path to the currently invoked binary +func (f *Factory) Executable() string { + ghPath := os.Getenv("GH_PATH") + if ghPath != "" { + return ghPath + } + if !strings.ContainsRune(f.ExecutableName, os.PathSeparator) { + f.ExecutableName = executable(f.ExecutableName) + } + return f.ExecutableName +} + +// Finds the location of the executable for the current process as it's found in PATH, respecting symlinks. +// If the process couldn't determine its location, return fallbackName. If the executable wasn't found in +// PATH, return the absolute location to the program. +// +// The idea is that the result of this function is callable in the future and refers to the same +// installation of gh, even across upgrades. This is needed primarily for Homebrew, which installs software +// under a location such as `/usr/local/Cellar/gh/1.13.1/bin/gh` and symlinks it from `/usr/local/bin/gh`. +// When the version is upgraded, Homebrew will often delete older versions, but keep the symlink. Because of +// this, we want to refer to the `gh` binary as `/usr/local/bin/gh` and not as its internal Homebrew +// location. +// +// None of this would be needed if we could just refer to GitHub CLI as `gh`, i.e. without using an absolute +// path. However, for some reason Homebrew does not include `/usr/local/bin` in PATH when it invokes git +// commands to update its taps. If `gh` (no path) is being used as git credential helper, as set up by `gh +// auth login`, running `brew update` will print out authentication errors as git is unable to locate +// Homebrew-installed `gh`. +func executable(fallbackName string) string { + exe, err := os.Executable() + if err != nil { + return fallbackName + } + + base := filepath.Base(exe) + path := os.Getenv("PATH") + for _, dir := range filepath.SplitList(path) { + p, err := filepath.Abs(filepath.Join(dir, base)) + if err != nil { + continue + } + f, err := os.Lstat(p) + if err != nil { + continue + } + + if p == exe { + return p + } else if f.Mode()&os.ModeSymlink != 0 { + realP, err := filepath.EvalSymlinks(p) + if err != nil { + continue + } + realExe, err := filepath.EvalSymlinks(exe) + if err != nil { + continue + } + if realP == realExe { + return p + } + } + } + + return exe +} diff --git a/vendor/github.com/cli/cli/v2/pkg/cmdutil/file_input.go b/vendor/github.com/cli/cli/v2/pkg/cmdutil/file_input.go new file mode 100644 index 000000000..e4cfd218a --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/cmdutil/file_input.go @@ -0,0 +1,16 @@ +package cmdutil + +import ( + "io" + "os" +) + +func ReadFile(filename string, stdin io.ReadCloser) ([]byte, error) { + if filename == "-" { + b, err := io.ReadAll(stdin) + _ = stdin.Close() + return b, err + } + + return os.ReadFile(filename) +} diff --git a/vendor/github.com/cli/cli/v2/pkg/cmdutil/flags.go b/vendor/github.com/cli/cli/v2/pkg/cmdutil/flags.go new file mode 100644 index 000000000..c0064099c --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/cmdutil/flags.go @@ -0,0 +1,182 @@ +package cmdutil + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// NilStringFlag defines a new flag with a string pointer receiver. This is useful for differentiating +// between the flag being set to a blank value and the flag not being passed at all. +func NilStringFlag(cmd *cobra.Command, p **string, name string, shorthand string, usage string) *pflag.Flag { + return cmd.Flags().VarPF(newStringValue(p), name, shorthand, usage) +} + +// NilBoolFlag defines a new flag with a bool pointer receiver. This is useful for differentiating +// between the flag being explicitly set to a false value and the flag not being passed at all. +func NilBoolFlag(cmd *cobra.Command, p **bool, name string, shorthand string, usage string) *pflag.Flag { + f := cmd.Flags().VarPF(newBoolValue(p), name, shorthand, usage) + f.NoOptDefVal = "true" + return f +} + +// StringEnumFlag defines a new string flag that only allows values listed in options. +func StringEnumFlag(cmd *cobra.Command, p *string, name, shorthand, defaultValue string, options []string, usage string) *pflag.Flag { + *p = defaultValue + val := &enumValue{string: p, options: options} + f := cmd.Flags().VarPF(val, name, shorthand, fmt.Sprintf("%s: %s", usage, formatValuesForUsageDocs(options))) + _ = cmd.RegisterFlagCompletionFunc(name, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return options, cobra.ShellCompDirectiveNoFileComp + }) + return f +} + +func StringSliceEnumFlag(cmd *cobra.Command, p *[]string, name, shorthand string, defaultValues, options []string, usage string) *pflag.Flag { + *p = defaultValues + val := &enumMultiValue{value: p, options: options} + f := cmd.Flags().VarPF(val, name, shorthand, fmt.Sprintf("%s: %s", usage, formatValuesForUsageDocs(options))) + _ = cmd.RegisterFlagCompletionFunc(name, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return options, cobra.ShellCompDirectiveNoFileComp + }) + return f +} + +type gitClient interface { + TrackingBranchNames(context.Context, string) []string +} + +// RegisterBranchCompletionFlags suggests and autocompletes known remote git branches for flags passed +func RegisterBranchCompletionFlags(gitc gitClient, cmd *cobra.Command, flags ...string) error { + for _, flag := range flags { + err := cmd.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if repoFlag := cmd.Flag("repo"); repoFlag != nil && repoFlag.Changed { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return gitc.TrackingBranchNames(context.TODO(), toComplete), cobra.ShellCompDirectiveNoFileComp + }) + if err != nil { + return err + } + } + return nil +} + +func formatValuesForUsageDocs(values []string) string { + return fmt.Sprintf("{%s}", strings.Join(values, "|")) +} + +type stringValue struct { + string **string +} + +func newStringValue(p **string) *stringValue { + return &stringValue{p} +} + +func (s *stringValue) Set(value string) error { + *s.string = &value + return nil +} + +func (s *stringValue) String() string { + if s.string == nil || *s.string == nil { + return "" + } + return **s.string +} + +func (s *stringValue) Type() string { + return "string" +} + +type boolValue struct { + bool **bool +} + +func newBoolValue(p **bool) *boolValue { + return &boolValue{p} +} + +func (b *boolValue) Set(value string) error { + v, err := strconv.ParseBool(value) + *b.bool = &v + return err +} + +func (b *boolValue) String() string { + if b.bool == nil || *b.bool == nil { + return "false" + } else if **b.bool { + return "true" + } + return "false" +} + +func (b *boolValue) Type() string { + return "bool" +} + +func (b *boolValue) IsBoolFlag() bool { + return true +} + +type enumValue struct { + string *string + options []string +} + +func (e *enumValue) Set(value string) error { + if !isIncluded(value, e.options) { + return fmt.Errorf("valid values are %s", formatValuesForUsageDocs(e.options)) + } + *e.string = value + return nil +} + +func (e *enumValue) String() string { + return *e.string +} + +func (e *enumValue) Type() string { + return "string" +} + +type enumMultiValue struct { + value *[]string + options []string +} + +func (e *enumMultiValue) Set(value string) error { + items := strings.Split(value, ",") + for _, item := range items { + if !isIncluded(item, e.options) { + return fmt.Errorf("valid values are %s", formatValuesForUsageDocs(e.options)) + } + } + *e.value = append(*e.value, items...) + return nil +} + +func (e *enumMultiValue) String() string { + if len(*e.value) == 0 { + return "" + } + return fmt.Sprintf("{%s}", strings.Join(*e.value, ", ")) +} + +func (e *enumMultiValue) Type() string { + return "stringSlice" +} + +func isIncluded(value string, opts []string) bool { + for _, opt := range opts { + if strings.EqualFold(opt, value) { + return true + } + } + return false +} diff --git a/vendor/github.com/cli/cli/v2/pkg/cmdutil/json_flags.go b/vendor/github.com/cli/cli/v2/pkg/cmdutil/json_flags.go new file mode 100644 index 000000000..bc3c242fe --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/cmdutil/json_flags.go @@ -0,0 +1,297 @@ +package cmdutil + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "reflect" + "sort" + "strings" + + "github.com/cli/cli/v2/pkg/iostreams" + "github.com/cli/cli/v2/pkg/jsoncolor" + "github.com/cli/cli/v2/pkg/set" + "github.com/cli/go-gh/v2/pkg/jq" + "github.com/cli/go-gh/v2/pkg/template" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +type JSONFlagError struct { + error +} + +func AddJSONFlags(cmd *cobra.Command, exportTarget *Exporter, fields []string) { + f := cmd.Flags() + f.StringSlice("json", nil, "Output JSON with the specified `fields`") + f.StringP("jq", "q", "", "Filter JSON output using a jq `expression`") + f.StringP("template", "t", "", "Format JSON output using a Go template; see \"gh help formatting\"") + + _ = cmd.RegisterFlagCompletionFunc("json", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var results []string + var prefix string + if idx := strings.LastIndexByte(toComplete, ','); idx >= 0 { + prefix = toComplete[:idx+1] + toComplete = toComplete[idx+1:] + } + toComplete = strings.ToLower(toComplete) + for _, f := range fields { + if strings.HasPrefix(strings.ToLower(f), toComplete) { + results = append(results, prefix+f) + } + } + sort.Strings(results) + return results, cobra.ShellCompDirectiveNoSpace + }) + + oldPreRun := cmd.PreRunE + cmd.PreRunE = func(c *cobra.Command, args []string) error { + if oldPreRun != nil { + if err := oldPreRun(c, args); err != nil { + return err + } + } + if export, err := checkJSONFlags(c); err == nil { + if export == nil { + *exportTarget = nil + } else { + allowedFields := set.NewStringSet() + allowedFields.AddValues(fields) + for _, f := range export.fields { + if !allowedFields.Contains(f) { + sort.Strings(fields) + return JSONFlagError{fmt.Errorf("Unknown JSON field: %q\nAvailable fields:\n %s", f, strings.Join(fields, "\n "))} + } + } + *exportTarget = export + } + } else { + return err + } + return nil + } + + cmd.SetFlagErrorFunc(func(c *cobra.Command, e error) error { + if c == cmd && e.Error() == "flag needs an argument: --json" { + sort.Strings(fields) + return JSONFlagError{fmt.Errorf("Specify one or more comma-separated fields for `--json`:\n %s", strings.Join(fields, "\n "))} + } + if cmd.HasParent() { + return cmd.Parent().FlagErrorFunc()(c, e) + } + return e + }) +} + +func checkJSONFlags(cmd *cobra.Command) (*jsonExporter, error) { + f := cmd.Flags() + jsonFlag := f.Lookup("json") + jqFlag := f.Lookup("jq") + tplFlag := f.Lookup("template") + webFlag := f.Lookup("web") + + if jsonFlag.Changed { + if webFlag != nil && webFlag.Changed { + return nil, errors.New("cannot use `--web` with `--json`") + } + jv := jsonFlag.Value.(pflag.SliceValue) + return &jsonExporter{ + fields: jv.GetSlice(), + filter: jqFlag.Value.String(), + template: tplFlag.Value.String(), + }, nil + } else if jqFlag.Changed { + return nil, errors.New("cannot use `--jq` without specifying `--json`") + } else if tplFlag.Changed { + return nil, errors.New("cannot use `--template` without specifying `--json`") + } + return nil, nil +} + +func AddFormatFlags(cmd *cobra.Command, exportTarget *Exporter) { + var format string + StringEnumFlag(cmd, &format, "format", "", "", []string{"json"}, "Output format") + f := cmd.Flags() + f.StringP("jq", "q", "", "Filter JSON output using a jq `expression`") + f.StringP("template", "t", "", "Format JSON output using a Go template; see \"gh help formatting\"") + + oldPreRun := cmd.PreRunE + cmd.PreRunE = func(c *cobra.Command, args []string) error { + if oldPreRun != nil { + if err := oldPreRun(c, args); err != nil { + return err + } + } + + if export, err := checkFormatFlags(c); err == nil { + if export == nil { + *exportTarget = nil + } else { + *exportTarget = export + } + } else { + return err + } + return nil + } +} + +func checkFormatFlags(cmd *cobra.Command) (*jsonExporter, error) { + f := cmd.Flags() + formatFlag := f.Lookup("format") + formatValue := formatFlag.Value.String() + jqFlag := f.Lookup("jq") + tplFlag := f.Lookup("template") + webFlag := f.Lookup("web") + + if formatFlag.Changed { + if webFlag != nil && webFlag.Changed { + return nil, errors.New("cannot use `--web` with `--format`") + } + return &jsonExporter{ + filter: jqFlag.Value.String(), + template: tplFlag.Value.String(), + }, nil + } else if jqFlag.Changed && formatValue != "json" { + return nil, errors.New("cannot use `--jq` without specifying `--format json`") + } else if tplFlag.Changed && formatValue != "json" { + return nil, errors.New("cannot use `--template` without specifying `--format json`") + } + return nil, nil +} + +type Exporter interface { + Fields() []string + Write(io *iostreams.IOStreams, data interface{}) error +} + +type jsonExporter struct { + fields []string + filter string + template string +} + +// NewJSONExporter returns an Exporter to emit JSON. +func NewJSONExporter() *jsonExporter { + return &jsonExporter{} +} + +func (e *jsonExporter) Fields() []string { + return e.fields +} + +func (e *jsonExporter) SetFields(fields []string) { + e.fields = fields +} + +// Write serializes data into JSON output written to w. If the object passed as data implements exportable, +// or if data is a map or slice of exportable object, ExportData() will be called on each object to obtain +// raw data for serialization. +func (e *jsonExporter) Write(ios *iostreams.IOStreams, data interface{}) error { + buf := bytes.Buffer{} + encoder := json.NewEncoder(&buf) + encoder.SetEscapeHTML(false) + if err := encoder.Encode(e.exportData(reflect.ValueOf(data))); err != nil { + return err + } + + w := ios.Out + if e.filter != "" { + indent := "" + if ios.IsStdoutTTY() { + indent = " " + } + if err := jq.EvaluateFormatted(&buf, w, e.filter, indent, ios.ColorEnabled()); err != nil { + return err + } + } else if e.template != "" { + t := template.New(w, ios.TerminalWidth(), ios.ColorEnabled()) + if err := t.Parse(e.template); err != nil { + return err + } + if err := t.Execute(&buf); err != nil { + return err + } + return t.Flush() + } else if ios.ColorEnabled() { + return jsoncolor.Write(w, &buf, " ") + } + + _, err := io.Copy(w, &buf) + return err +} + +func (e *jsonExporter) exportData(v reflect.Value) interface{} { + switch v.Kind() { + case reflect.Ptr, reflect.Interface: + if !v.IsNil() { + return e.exportData(v.Elem()) + } + case reflect.Slice: + a := make([]interface{}, v.Len()) + for i := 0; i < v.Len(); i++ { + a[i] = e.exportData(v.Index(i)) + } + return a + case reflect.Map: + t := reflect.MapOf(v.Type().Key(), emptyInterfaceType) + m := reflect.MakeMapWithSize(t, v.Len()) + iter := v.MapRange() + for iter.Next() { + ve := reflect.ValueOf(e.exportData(iter.Value())) + m.SetMapIndex(iter.Key(), ve) + } + return m.Interface() + case reflect.Struct: + if v.CanAddr() && reflect.PtrTo(v.Type()).Implements(exportableType) { + ve := v.Addr().Interface().(exportable) + return ve.ExportData(e.fields) + } else if v.Type().Implements(exportableType) { + ve := v.Interface().(exportable) + return ve.ExportData(e.fields) + } + } + return v.Interface() +} + +type exportable interface { + ExportData([]string) map[string]interface{} +} + +var exportableType = reflect.TypeOf((*exportable)(nil)).Elem() +var sliceOfEmptyInterface []interface{} +var emptyInterfaceType = reflect.TypeOf(sliceOfEmptyInterface).Elem() + +// Basic function that can be used with structs that need to implement +// the exportable interface. It has numerous limitations so verify +// that it works as expected with the struct and fields you want to export. +// If it does not, then implementing a custom ExportData method is necessary. +// Perhaps this should be moved up into exportData for the case when +// a struct does not implement the exportable interface, but for now it will +// need to be explicitly used. +func StructExportData(s interface{}, fields []string) map[string]interface{} { + v := reflect.ValueOf(s) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + if v.Kind() != reflect.Struct { + // If s is not a struct or pointer to a struct return nil. + return nil + } + data := make(map[string]interface{}, len(fields)) + for _, f := range fields { + sf := fieldByName(v, f) + if sf.IsValid() && sf.CanInterface() { + data[f] = sf.Interface() + } + } + return data +} + +func fieldByName(v reflect.Value, field string) reflect.Value { + return v.FieldByNameFunc(func(s string) bool { + return strings.EqualFold(field, s) + }) +} diff --git a/vendor/github.com/cli/cli/v2/pkg/cmdutil/legacy.go b/vendor/github.com/cli/cli/v2/pkg/cmdutil/legacy.go new file mode 100644 index 000000000..1f247d4e6 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/cmdutil/legacy.go @@ -0,0 +1,23 @@ +package cmdutil + +import ( + "fmt" + "os" + + "github.com/cli/cli/v2/internal/config" +) + +// TODO: consider passing via Factory +// TODO: support per-hostname settings +func DetermineEditor(cf func() (config.Config, error)) (string, error) { + editorCommand := os.Getenv("GH_EDITOR") + if editorCommand == "" { + cfg, err := cf() + if err != nil { + return "", fmt.Errorf("could not read config: %w", err) + } + editorCommand = cfg.Editor("") + } + + return editorCommand, nil +} diff --git a/vendor/github.com/cli/cli/v2/pkg/cmdutil/repo_override.go b/vendor/github.com/cli/cli/v2/pkg/cmdutil/repo_override.go new file mode 100644 index 000000000..791dd919a --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/cmdutil/repo_override.go @@ -0,0 +1,70 @@ +package cmdutil + +import ( + "os" + "sort" + "strings" + + "github.com/cli/cli/v2/internal/ghrepo" + "github.com/spf13/cobra" +) + +func executeParentHooks(cmd *cobra.Command, args []string) error { + for cmd.HasParent() { + cmd = cmd.Parent() + if cmd.PersistentPreRunE != nil { + return cmd.PersistentPreRunE(cmd, args) + } + } + return nil +} + +func EnableRepoOverride(cmd *cobra.Command, f *Factory) { + cmd.PersistentFlags().StringP("repo", "R", "", "Select another repository using the `[HOST/]OWNER/REPO` format") + _ = cmd.RegisterFlagCompletionFunc("repo", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + remotes, err := f.Remotes() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + config, err := f.Config() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + defaultHost, _ := config.Authentication().DefaultHost() + + var results []string + for _, remote := range remotes { + repo := remote.RepoOwner() + "/" + remote.RepoName() + if !strings.EqualFold(remote.RepoHost(), defaultHost) { + repo = remote.RepoHost() + "/" + repo + } + if strings.HasPrefix(repo, toComplete) { + results = append(results, repo) + } + } + sort.Strings(results) + return results, cobra.ShellCompDirectiveNoFileComp + }) + + cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + if err := executeParentHooks(cmd, args); err != nil { + return err + } + repoOverride, _ := cmd.Flags().GetString("repo") + f.BaseRepo = OverrideBaseRepoFunc(f, repoOverride) + return nil + } +} + +func OverrideBaseRepoFunc(f *Factory, override string) func() (ghrepo.Interface, error) { + if override == "" { + override = os.Getenv("GH_REPO") + } + if override != "" { + return func() (ghrepo.Interface, error) { + return ghrepo.FromFullName(override) + } + } + return f.BaseRepo +} diff --git a/vendor/github.com/cli/cli/v2/pkg/extensions/extension.go b/vendor/github.com/cli/cli/v2/pkg/extensions/extension.go new file mode 100644 index 000000000..0b94032df --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/extensions/extension.go @@ -0,0 +1,41 @@ +package extensions + +import ( + "io" + + "github.com/cli/cli/v2/internal/ghrepo" +) + +type ExtTemplateType int + +const ( + GitTemplateType ExtTemplateType = 0 + GoBinTemplateType ExtTemplateType = 1 + OtherBinTemplateType ExtTemplateType = 2 +) + +//go:generate moq -rm -out extension_mock.go . Extension +type Extension interface { + Name() string // Extension Name without gh- + Path() string // Path to executable + URL() string + CurrentVersion() string + LatestVersion() string + IsPinned() bool + UpdateAvailable() bool + IsBinary() bool + IsLocal() bool + Owner() string +} + +//go:generate moq -rm -out manager_mock.go . ExtensionManager +type ExtensionManager interface { + List() []Extension + Install(ghrepo.Interface, string) error + InstallLocal(dir string) error + Upgrade(name string, force bool) error + Remove(name string) error + Dispatch(args []string, stdin io.Reader, stdout, stderr io.Writer) (bool, error) + Create(name string, tmplType ExtTemplateType) error + EnableDryRunMode() +} diff --git a/vendor/github.com/cli/cli/v2/pkg/extensions/extension_mock.go b/vendor/github.com/cli/cli/v2/pkg/extensions/extension_mock.go new file mode 100644 index 000000000..9b473e20a --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/extensions/extension_mock.go @@ -0,0 +1,400 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package extensions + +import ( + "sync" +) + +// Ensure, that ExtensionMock does implement Extension. +// If this is not the case, regenerate this file with moq. +var _ Extension = &ExtensionMock{} + +// ExtensionMock is a mock implementation of Extension. +// +// func TestSomethingThatUsesExtension(t *testing.T) { +// +// // make and configure a mocked Extension +// mockedExtension := &ExtensionMock{ +// CurrentVersionFunc: func() string { +// panic("mock out the CurrentVersion method") +// }, +// IsBinaryFunc: func() bool { +// panic("mock out the IsBinary method") +// }, +// IsLocalFunc: func() bool { +// panic("mock out the IsLocal method") +// }, +// IsPinnedFunc: func() bool { +// panic("mock out the IsPinned method") +// }, +// LatestVersionFunc: func() string { +// panic("mock out the LatestVersion method") +// }, +// NameFunc: func() string { +// panic("mock out the Name method") +// }, +// OwnerFunc: func() string { +// panic("mock out the Owner method") +// }, +// PathFunc: func() string { +// panic("mock out the Path method") +// }, +// URLFunc: func() string { +// panic("mock out the URL method") +// }, +// UpdateAvailableFunc: func() bool { +// panic("mock out the UpdateAvailable method") +// }, +// } +// +// // use mockedExtension in code that requires Extension +// // and then make assertions. +// +// } +type ExtensionMock struct { + // CurrentVersionFunc mocks the CurrentVersion method. + CurrentVersionFunc func() string + + // IsBinaryFunc mocks the IsBinary method. + IsBinaryFunc func() bool + + // IsLocalFunc mocks the IsLocal method. + IsLocalFunc func() bool + + // IsPinnedFunc mocks the IsPinned method. + IsPinnedFunc func() bool + + // LatestVersionFunc mocks the LatestVersion method. + LatestVersionFunc func() string + + // NameFunc mocks the Name method. + NameFunc func() string + + // OwnerFunc mocks the Owner method. + OwnerFunc func() string + + // PathFunc mocks the Path method. + PathFunc func() string + + // URLFunc mocks the URL method. + URLFunc func() string + + // UpdateAvailableFunc mocks the UpdateAvailable method. + UpdateAvailableFunc func() bool + + // calls tracks calls to the methods. + calls struct { + // CurrentVersion holds details about calls to the CurrentVersion method. + CurrentVersion []struct { + } + // IsBinary holds details about calls to the IsBinary method. + IsBinary []struct { + } + // IsLocal holds details about calls to the IsLocal method. + IsLocal []struct { + } + // IsPinned holds details about calls to the IsPinned method. + IsPinned []struct { + } + // LatestVersion holds details about calls to the LatestVersion method. + LatestVersion []struct { + } + // Name holds details about calls to the Name method. + Name []struct { + } + // Owner holds details about calls to the Owner method. + Owner []struct { + } + // Path holds details about calls to the Path method. + Path []struct { + } + // URL holds details about calls to the URL method. + URL []struct { + } + // UpdateAvailable holds details about calls to the UpdateAvailable method. + UpdateAvailable []struct { + } + } + lockCurrentVersion sync.RWMutex + lockIsBinary sync.RWMutex + lockIsLocal sync.RWMutex + lockIsPinned sync.RWMutex + lockLatestVersion sync.RWMutex + lockName sync.RWMutex + lockOwner sync.RWMutex + lockPath sync.RWMutex + lockURL sync.RWMutex + lockUpdateAvailable sync.RWMutex +} + +// CurrentVersion calls CurrentVersionFunc. +func (mock *ExtensionMock) CurrentVersion() string { + if mock.CurrentVersionFunc == nil { + panic("ExtensionMock.CurrentVersionFunc: method is nil but Extension.CurrentVersion was just called") + } + callInfo := struct { + }{} + mock.lockCurrentVersion.Lock() + mock.calls.CurrentVersion = append(mock.calls.CurrentVersion, callInfo) + mock.lockCurrentVersion.Unlock() + return mock.CurrentVersionFunc() +} + +// CurrentVersionCalls gets all the calls that were made to CurrentVersion. +// Check the length with: +// +// len(mockedExtension.CurrentVersionCalls()) +func (mock *ExtensionMock) CurrentVersionCalls() []struct { +} { + var calls []struct { + } + mock.lockCurrentVersion.RLock() + calls = mock.calls.CurrentVersion + mock.lockCurrentVersion.RUnlock() + return calls +} + +// IsBinary calls IsBinaryFunc. +func (mock *ExtensionMock) IsBinary() bool { + if mock.IsBinaryFunc == nil { + panic("ExtensionMock.IsBinaryFunc: method is nil but Extension.IsBinary was just called") + } + callInfo := struct { + }{} + mock.lockIsBinary.Lock() + mock.calls.IsBinary = append(mock.calls.IsBinary, callInfo) + mock.lockIsBinary.Unlock() + return mock.IsBinaryFunc() +} + +// IsBinaryCalls gets all the calls that were made to IsBinary. +// Check the length with: +// +// len(mockedExtension.IsBinaryCalls()) +func (mock *ExtensionMock) IsBinaryCalls() []struct { +} { + var calls []struct { + } + mock.lockIsBinary.RLock() + calls = mock.calls.IsBinary + mock.lockIsBinary.RUnlock() + return calls +} + +// IsLocal calls IsLocalFunc. +func (mock *ExtensionMock) IsLocal() bool { + if mock.IsLocalFunc == nil { + panic("ExtensionMock.IsLocalFunc: method is nil but Extension.IsLocal was just called") + } + callInfo := struct { + }{} + mock.lockIsLocal.Lock() + mock.calls.IsLocal = append(mock.calls.IsLocal, callInfo) + mock.lockIsLocal.Unlock() + return mock.IsLocalFunc() +} + +// IsLocalCalls gets all the calls that were made to IsLocal. +// Check the length with: +// +// len(mockedExtension.IsLocalCalls()) +func (mock *ExtensionMock) IsLocalCalls() []struct { +} { + var calls []struct { + } + mock.lockIsLocal.RLock() + calls = mock.calls.IsLocal + mock.lockIsLocal.RUnlock() + return calls +} + +// IsPinned calls IsPinnedFunc. +func (mock *ExtensionMock) IsPinned() bool { + if mock.IsPinnedFunc == nil { + panic("ExtensionMock.IsPinnedFunc: method is nil but Extension.IsPinned was just called") + } + callInfo := struct { + }{} + mock.lockIsPinned.Lock() + mock.calls.IsPinned = append(mock.calls.IsPinned, callInfo) + mock.lockIsPinned.Unlock() + return mock.IsPinnedFunc() +} + +// IsPinnedCalls gets all the calls that were made to IsPinned. +// Check the length with: +// +// len(mockedExtension.IsPinnedCalls()) +func (mock *ExtensionMock) IsPinnedCalls() []struct { +} { + var calls []struct { + } + mock.lockIsPinned.RLock() + calls = mock.calls.IsPinned + mock.lockIsPinned.RUnlock() + return calls +} + +// LatestVersion calls LatestVersionFunc. +func (mock *ExtensionMock) LatestVersion() string { + if mock.LatestVersionFunc == nil { + panic("ExtensionMock.LatestVersionFunc: method is nil but Extension.LatestVersion was just called") + } + callInfo := struct { + }{} + mock.lockLatestVersion.Lock() + mock.calls.LatestVersion = append(mock.calls.LatestVersion, callInfo) + mock.lockLatestVersion.Unlock() + return mock.LatestVersionFunc() +} + +// LatestVersionCalls gets all the calls that were made to LatestVersion. +// Check the length with: +// +// len(mockedExtension.LatestVersionCalls()) +func (mock *ExtensionMock) LatestVersionCalls() []struct { +} { + var calls []struct { + } + mock.lockLatestVersion.RLock() + calls = mock.calls.LatestVersion + mock.lockLatestVersion.RUnlock() + return calls +} + +// Name calls NameFunc. +func (mock *ExtensionMock) Name() string { + if mock.NameFunc == nil { + panic("ExtensionMock.NameFunc: method is nil but Extension.Name was just called") + } + callInfo := struct { + }{} + mock.lockName.Lock() + mock.calls.Name = append(mock.calls.Name, callInfo) + mock.lockName.Unlock() + return mock.NameFunc() +} + +// NameCalls gets all the calls that were made to Name. +// Check the length with: +// +// len(mockedExtension.NameCalls()) +func (mock *ExtensionMock) NameCalls() []struct { +} { + var calls []struct { + } + mock.lockName.RLock() + calls = mock.calls.Name + mock.lockName.RUnlock() + return calls +} + +// Owner calls OwnerFunc. +func (mock *ExtensionMock) Owner() string { + if mock.OwnerFunc == nil { + panic("ExtensionMock.OwnerFunc: method is nil but Extension.Owner was just called") + } + callInfo := struct { + }{} + mock.lockOwner.Lock() + mock.calls.Owner = append(mock.calls.Owner, callInfo) + mock.lockOwner.Unlock() + return mock.OwnerFunc() +} + +// OwnerCalls gets all the calls that were made to Owner. +// Check the length with: +// +// len(mockedExtension.OwnerCalls()) +func (mock *ExtensionMock) OwnerCalls() []struct { +} { + var calls []struct { + } + mock.lockOwner.RLock() + calls = mock.calls.Owner + mock.lockOwner.RUnlock() + return calls +} + +// Path calls PathFunc. +func (mock *ExtensionMock) Path() string { + if mock.PathFunc == nil { + panic("ExtensionMock.PathFunc: method is nil but Extension.Path was just called") + } + callInfo := struct { + }{} + mock.lockPath.Lock() + mock.calls.Path = append(mock.calls.Path, callInfo) + mock.lockPath.Unlock() + return mock.PathFunc() +} + +// PathCalls gets all the calls that were made to Path. +// Check the length with: +// +// len(mockedExtension.PathCalls()) +func (mock *ExtensionMock) PathCalls() []struct { +} { + var calls []struct { + } + mock.lockPath.RLock() + calls = mock.calls.Path + mock.lockPath.RUnlock() + return calls +} + +// URL calls URLFunc. +func (mock *ExtensionMock) URL() string { + if mock.URLFunc == nil { + panic("ExtensionMock.URLFunc: method is nil but Extension.URL was just called") + } + callInfo := struct { + }{} + mock.lockURL.Lock() + mock.calls.URL = append(mock.calls.URL, callInfo) + mock.lockURL.Unlock() + return mock.URLFunc() +} + +// URLCalls gets all the calls that were made to URL. +// Check the length with: +// +// len(mockedExtension.URLCalls()) +func (mock *ExtensionMock) URLCalls() []struct { +} { + var calls []struct { + } + mock.lockURL.RLock() + calls = mock.calls.URL + mock.lockURL.RUnlock() + return calls +} + +// UpdateAvailable calls UpdateAvailableFunc. +func (mock *ExtensionMock) UpdateAvailable() bool { + if mock.UpdateAvailableFunc == nil { + panic("ExtensionMock.UpdateAvailableFunc: method is nil but Extension.UpdateAvailable was just called") + } + callInfo := struct { + }{} + mock.lockUpdateAvailable.Lock() + mock.calls.UpdateAvailable = append(mock.calls.UpdateAvailable, callInfo) + mock.lockUpdateAvailable.Unlock() + return mock.UpdateAvailableFunc() +} + +// UpdateAvailableCalls gets all the calls that were made to UpdateAvailable. +// Check the length with: +// +// len(mockedExtension.UpdateAvailableCalls()) +func (mock *ExtensionMock) UpdateAvailableCalls() []struct { +} { + var calls []struct { + } + mock.lockUpdateAvailable.RLock() + calls = mock.calls.UpdateAvailable + mock.lockUpdateAvailable.RUnlock() + return calls +} diff --git a/vendor/github.com/cli/cli/v2/pkg/extensions/manager_mock.go b/vendor/github.com/cli/cli/v2/pkg/extensions/manager_mock.go new file mode 100644 index 000000000..962444f2b --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/extensions/manager_mock.go @@ -0,0 +1,406 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package extensions + +import ( + "github.com/cli/cli/v2/internal/ghrepo" + "io" + "sync" +) + +// Ensure, that ExtensionManagerMock does implement ExtensionManager. +// If this is not the case, regenerate this file with moq. +var _ ExtensionManager = &ExtensionManagerMock{} + +// ExtensionManagerMock is a mock implementation of ExtensionManager. +// +// func TestSomethingThatUsesExtensionManager(t *testing.T) { +// +// // make and configure a mocked ExtensionManager +// mockedExtensionManager := &ExtensionManagerMock{ +// CreateFunc: func(name string, tmplType ExtTemplateType) error { +// panic("mock out the Create method") +// }, +// DispatchFunc: func(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (bool, error) { +// panic("mock out the Dispatch method") +// }, +// EnableDryRunModeFunc: func() { +// panic("mock out the EnableDryRunMode method") +// }, +// InstallFunc: func(interfaceMoqParam ghrepo.Interface, s string) error { +// panic("mock out the Install method") +// }, +// InstallLocalFunc: func(dir string) error { +// panic("mock out the InstallLocal method") +// }, +// ListFunc: func() []Extension { +// panic("mock out the List method") +// }, +// RemoveFunc: func(name string) error { +// panic("mock out the Remove method") +// }, +// UpgradeFunc: func(name string, force bool) error { +// panic("mock out the Upgrade method") +// }, +// } +// +// // use mockedExtensionManager in code that requires ExtensionManager +// // and then make assertions. +// +// } +type ExtensionManagerMock struct { + // CreateFunc mocks the Create method. + CreateFunc func(name string, tmplType ExtTemplateType) error + + // DispatchFunc mocks the Dispatch method. + DispatchFunc func(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (bool, error) + + // EnableDryRunModeFunc mocks the EnableDryRunMode method. + EnableDryRunModeFunc func() + + // InstallFunc mocks the Install method. + InstallFunc func(interfaceMoqParam ghrepo.Interface, s string) error + + // InstallLocalFunc mocks the InstallLocal method. + InstallLocalFunc func(dir string) error + + // ListFunc mocks the List method. + ListFunc func() []Extension + + // RemoveFunc mocks the Remove method. + RemoveFunc func(name string) error + + // UpgradeFunc mocks the Upgrade method. + UpgradeFunc func(name string, force bool) error + + // calls tracks calls to the methods. + calls struct { + // Create holds details about calls to the Create method. + Create []struct { + // Name is the name argument value. + Name string + // TmplType is the tmplType argument value. + TmplType ExtTemplateType + } + // Dispatch holds details about calls to the Dispatch method. + Dispatch []struct { + // Args is the args argument value. + Args []string + // Stdin is the stdin argument value. + Stdin io.Reader + // Stdout is the stdout argument value. + Stdout io.Writer + // Stderr is the stderr argument value. + Stderr io.Writer + } + // EnableDryRunMode holds details about calls to the EnableDryRunMode method. + EnableDryRunMode []struct { + } + // Install holds details about calls to the Install method. + Install []struct { + // InterfaceMoqParam is the interfaceMoqParam argument value. + InterfaceMoqParam ghrepo.Interface + // S is the s argument value. + S string + } + // InstallLocal holds details about calls to the InstallLocal method. + InstallLocal []struct { + // Dir is the dir argument value. + Dir string + } + // List holds details about calls to the List method. + List []struct { + } + // Remove holds details about calls to the Remove method. + Remove []struct { + // Name is the name argument value. + Name string + } + // Upgrade holds details about calls to the Upgrade method. + Upgrade []struct { + // Name is the name argument value. + Name string + // Force is the force argument value. + Force bool + } + } + lockCreate sync.RWMutex + lockDispatch sync.RWMutex + lockEnableDryRunMode sync.RWMutex + lockInstall sync.RWMutex + lockInstallLocal sync.RWMutex + lockList sync.RWMutex + lockRemove sync.RWMutex + lockUpgrade sync.RWMutex +} + +// Create calls CreateFunc. +func (mock *ExtensionManagerMock) Create(name string, tmplType ExtTemplateType) error { + if mock.CreateFunc == nil { + panic("ExtensionManagerMock.CreateFunc: method is nil but ExtensionManager.Create was just called") + } + callInfo := struct { + Name string + TmplType ExtTemplateType + }{ + Name: name, + TmplType: tmplType, + } + mock.lockCreate.Lock() + mock.calls.Create = append(mock.calls.Create, callInfo) + mock.lockCreate.Unlock() + return mock.CreateFunc(name, tmplType) +} + +// CreateCalls gets all the calls that were made to Create. +// Check the length with: +// +// len(mockedExtensionManager.CreateCalls()) +func (mock *ExtensionManagerMock) CreateCalls() []struct { + Name string + TmplType ExtTemplateType +} { + var calls []struct { + Name string + TmplType ExtTemplateType + } + mock.lockCreate.RLock() + calls = mock.calls.Create + mock.lockCreate.RUnlock() + return calls +} + +// Dispatch calls DispatchFunc. +func (mock *ExtensionManagerMock) Dispatch(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (bool, error) { + if mock.DispatchFunc == nil { + panic("ExtensionManagerMock.DispatchFunc: method is nil but ExtensionManager.Dispatch was just called") + } + callInfo := struct { + Args []string + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer + }{ + Args: args, + Stdin: stdin, + Stdout: stdout, + Stderr: stderr, + } + mock.lockDispatch.Lock() + mock.calls.Dispatch = append(mock.calls.Dispatch, callInfo) + mock.lockDispatch.Unlock() + return mock.DispatchFunc(args, stdin, stdout, stderr) +} + +// DispatchCalls gets all the calls that were made to Dispatch. +// Check the length with: +// +// len(mockedExtensionManager.DispatchCalls()) +func (mock *ExtensionManagerMock) DispatchCalls() []struct { + Args []string + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer +} { + var calls []struct { + Args []string + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer + } + mock.lockDispatch.RLock() + calls = mock.calls.Dispatch + mock.lockDispatch.RUnlock() + return calls +} + +// EnableDryRunMode calls EnableDryRunModeFunc. +func (mock *ExtensionManagerMock) EnableDryRunMode() { + if mock.EnableDryRunModeFunc == nil { + panic("ExtensionManagerMock.EnableDryRunModeFunc: method is nil but ExtensionManager.EnableDryRunMode was just called") + } + callInfo := struct { + }{} + mock.lockEnableDryRunMode.Lock() + mock.calls.EnableDryRunMode = append(mock.calls.EnableDryRunMode, callInfo) + mock.lockEnableDryRunMode.Unlock() + mock.EnableDryRunModeFunc() +} + +// EnableDryRunModeCalls gets all the calls that were made to EnableDryRunMode. +// Check the length with: +// +// len(mockedExtensionManager.EnableDryRunModeCalls()) +func (mock *ExtensionManagerMock) EnableDryRunModeCalls() []struct { +} { + var calls []struct { + } + mock.lockEnableDryRunMode.RLock() + calls = mock.calls.EnableDryRunMode + mock.lockEnableDryRunMode.RUnlock() + return calls +} + +// Install calls InstallFunc. +func (mock *ExtensionManagerMock) Install(interfaceMoqParam ghrepo.Interface, s string) error { + if mock.InstallFunc == nil { + panic("ExtensionManagerMock.InstallFunc: method is nil but ExtensionManager.Install was just called") + } + callInfo := struct { + InterfaceMoqParam ghrepo.Interface + S string + }{ + InterfaceMoqParam: interfaceMoqParam, + S: s, + } + mock.lockInstall.Lock() + mock.calls.Install = append(mock.calls.Install, callInfo) + mock.lockInstall.Unlock() + return mock.InstallFunc(interfaceMoqParam, s) +} + +// InstallCalls gets all the calls that were made to Install. +// Check the length with: +// +// len(mockedExtensionManager.InstallCalls()) +func (mock *ExtensionManagerMock) InstallCalls() []struct { + InterfaceMoqParam ghrepo.Interface + S string +} { + var calls []struct { + InterfaceMoqParam ghrepo.Interface + S string + } + mock.lockInstall.RLock() + calls = mock.calls.Install + mock.lockInstall.RUnlock() + return calls +} + +// InstallLocal calls InstallLocalFunc. +func (mock *ExtensionManagerMock) InstallLocal(dir string) error { + if mock.InstallLocalFunc == nil { + panic("ExtensionManagerMock.InstallLocalFunc: method is nil but ExtensionManager.InstallLocal was just called") + } + callInfo := struct { + Dir string + }{ + Dir: dir, + } + mock.lockInstallLocal.Lock() + mock.calls.InstallLocal = append(mock.calls.InstallLocal, callInfo) + mock.lockInstallLocal.Unlock() + return mock.InstallLocalFunc(dir) +} + +// InstallLocalCalls gets all the calls that were made to InstallLocal. +// Check the length with: +// +// len(mockedExtensionManager.InstallLocalCalls()) +func (mock *ExtensionManagerMock) InstallLocalCalls() []struct { + Dir string +} { + var calls []struct { + Dir string + } + mock.lockInstallLocal.RLock() + calls = mock.calls.InstallLocal + mock.lockInstallLocal.RUnlock() + return calls +} + +// List calls ListFunc. +func (mock *ExtensionManagerMock) List() []Extension { + if mock.ListFunc == nil { + panic("ExtensionManagerMock.ListFunc: method is nil but ExtensionManager.List was just called") + } + callInfo := struct { + }{} + mock.lockList.Lock() + mock.calls.List = append(mock.calls.List, callInfo) + mock.lockList.Unlock() + return mock.ListFunc() +} + +// ListCalls gets all the calls that were made to List. +// Check the length with: +// +// len(mockedExtensionManager.ListCalls()) +func (mock *ExtensionManagerMock) ListCalls() []struct { +} { + var calls []struct { + } + mock.lockList.RLock() + calls = mock.calls.List + mock.lockList.RUnlock() + return calls +} + +// Remove calls RemoveFunc. +func (mock *ExtensionManagerMock) Remove(name string) error { + if mock.RemoveFunc == nil { + panic("ExtensionManagerMock.RemoveFunc: method is nil but ExtensionManager.Remove was just called") + } + callInfo := struct { + Name string + }{ + Name: name, + } + mock.lockRemove.Lock() + mock.calls.Remove = append(mock.calls.Remove, callInfo) + mock.lockRemove.Unlock() + return mock.RemoveFunc(name) +} + +// RemoveCalls gets all the calls that were made to Remove. +// Check the length with: +// +// len(mockedExtensionManager.RemoveCalls()) +func (mock *ExtensionManagerMock) RemoveCalls() []struct { + Name string +} { + var calls []struct { + Name string + } + mock.lockRemove.RLock() + calls = mock.calls.Remove + mock.lockRemove.RUnlock() + return calls +} + +// Upgrade calls UpgradeFunc. +func (mock *ExtensionManagerMock) Upgrade(name string, force bool) error { + if mock.UpgradeFunc == nil { + panic("ExtensionManagerMock.UpgradeFunc: method is nil but ExtensionManager.Upgrade was just called") + } + callInfo := struct { + Name string + Force bool + }{ + Name: name, + Force: force, + } + mock.lockUpgrade.Lock() + mock.calls.Upgrade = append(mock.calls.Upgrade, callInfo) + mock.lockUpgrade.Unlock() + return mock.UpgradeFunc(name, force) +} + +// UpgradeCalls gets all the calls that were made to Upgrade. +// Check the length with: +// +// len(mockedExtensionManager.UpgradeCalls()) +func (mock *ExtensionManagerMock) UpgradeCalls() []struct { + Name string + Force bool +} { + var calls []struct { + Name string + Force bool + } + mock.lockUpgrade.RLock() + calls = mock.calls.Upgrade + mock.lockUpgrade.RUnlock() + return calls +} diff --git a/vendor/github.com/cli/cli/v2/pkg/iostreams/color.go b/vendor/github.com/cli/cli/v2/pkg/iostreams/color.go new file mode 100644 index 000000000..804b9a275 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/iostreams/color.go @@ -0,0 +1,226 @@ +package iostreams + +import ( + "fmt" + "strconv" + "strings" + + "github.com/mgutz/ansi" +) + +var ( + magenta = ansi.ColorFunc("magenta") + cyan = ansi.ColorFunc("cyan") + red = ansi.ColorFunc("red") + yellow = ansi.ColorFunc("yellow") + blue = ansi.ColorFunc("blue") + green = ansi.ColorFunc("green") + gray = ansi.ColorFunc("black+h") + lightGrayUnderline = ansi.ColorFunc("white+du") + bold = ansi.ColorFunc("default+b") + cyanBold = ansi.ColorFunc("cyan+b") + greenBold = ansi.ColorFunc("green+b") + + gray256 = func(t string) string { + return fmt.Sprintf("\x1b[%d;5;%dm%s\x1b[m", 38, 242, t) + } +) + +func NewColorScheme(enabled, is256enabled bool, trueColor bool) *ColorScheme { + return &ColorScheme{ + enabled: enabled, + is256enabled: is256enabled, + hasTrueColor: trueColor, + } +} + +type ColorScheme struct { + enabled bool + is256enabled bool + hasTrueColor bool +} + +func (c *ColorScheme) Enabled() bool { + return c.enabled +} + +func (c *ColorScheme) Bold(t string) string { + if !c.enabled { + return t + } + return bold(t) +} + +func (c *ColorScheme) Boldf(t string, args ...interface{}) string { + return c.Bold(fmt.Sprintf(t, args...)) +} + +func (c *ColorScheme) Red(t string) string { + if !c.enabled { + return t + } + return red(t) +} + +func (c *ColorScheme) Redf(t string, args ...interface{}) string { + return c.Red(fmt.Sprintf(t, args...)) +} + +func (c *ColorScheme) Yellow(t string) string { + if !c.enabled { + return t + } + return yellow(t) +} + +func (c *ColorScheme) Yellowf(t string, args ...interface{}) string { + return c.Yellow(fmt.Sprintf(t, args...)) +} + +func (c *ColorScheme) Green(t string) string { + if !c.enabled { + return t + } + return green(t) +} + +func (c *ColorScheme) Greenf(t string, args ...interface{}) string { + return c.Green(fmt.Sprintf(t, args...)) +} + +func (c *ColorScheme) GreenBold(t string) string { + if !c.enabled { + return t + } + return greenBold(t) +} + +func (c *ColorScheme) Gray(t string) string { + if !c.enabled { + return t + } + if c.is256enabled { + return gray256(t) + } + return gray(t) +} + +func (c *ColorScheme) Grayf(t string, args ...interface{}) string { + return c.Gray(fmt.Sprintf(t, args...)) +} + +func (c *ColorScheme) LightGrayUnderline(t string) string { + if !c.enabled { + return t + } + return lightGrayUnderline(t) +} + +func (c *ColorScheme) Magenta(t string) string { + if !c.enabled { + return t + } + return magenta(t) +} + +func (c *ColorScheme) Magentaf(t string, args ...interface{}) string { + return c.Magenta(fmt.Sprintf(t, args...)) +} + +func (c *ColorScheme) Cyan(t string) string { + if !c.enabled { + return t + } + return cyan(t) +} + +func (c *ColorScheme) Cyanf(t string, args ...interface{}) string { + return c.Cyan(fmt.Sprintf(t, args...)) +} + +func (c *ColorScheme) CyanBold(t string) string { + if !c.enabled { + return t + } + return cyanBold(t) +} + +func (c *ColorScheme) Blue(t string) string { + if !c.enabled { + return t + } + return blue(t) +} + +func (c *ColorScheme) Bluef(t string, args ...interface{}) string { + return c.Blue(fmt.Sprintf(t, args...)) +} + +func (c *ColorScheme) SuccessIcon() string { + return c.SuccessIconWithColor(c.Green) +} + +func (c *ColorScheme) SuccessIconWithColor(colo func(string) string) string { + return colo("✓") +} + +func (c *ColorScheme) WarningIcon() string { + return c.Yellow("!") +} + +func (c *ColorScheme) FailureIcon() string { + return c.FailureIconWithColor(c.Red) +} + +func (c *ColorScheme) FailureIconWithColor(colo func(string) string) string { + return colo("X") +} + +func (c *ColorScheme) ColorFromString(s string) func(string) string { + s = strings.ToLower(s) + var fn func(string) string + switch s { + case "bold": + fn = c.Bold + case "red": + fn = c.Red + case "yellow": + fn = c.Yellow + case "green": + fn = c.Green + case "gray": + fn = c.Gray + case "magenta": + fn = c.Magenta + case "cyan": + fn = c.Cyan + case "blue": + fn = c.Blue + default: + fn = func(s string) string { + return s + } + } + + return fn +} + +// ColorFromRGB returns a function suitable for TablePrinter.AddField +// that calls HexToRGB, coloring text if supported by the terminal. +func (c *ColorScheme) ColorFromRGB(hex string) func(string) string { + return func(s string) string { + return c.HexToRGB(hex, s) + } +} + +// HexToRGB uses the given hex to color x if supported by the terminal. +func (c *ColorScheme) HexToRGB(hex string, x string) string { + if !c.enabled || !c.hasTrueColor || len(hex) != 6 { + return x + } + + r, _ := strconv.ParseInt(hex[0:2], 16, 64) + g, _ := strconv.ParseInt(hex[2:4], 16, 64) + b, _ := strconv.ParseInt(hex[4:6], 16, 64) + return fmt.Sprintf("\033[38;2;%d;%d;%dm%s\033[0m", r, g, b, x) +} diff --git a/vendor/github.com/cli/cli/v2/pkg/iostreams/console.go b/vendor/github.com/cli/cli/v2/pkg/iostreams/console.go new file mode 100644 index 000000000..89bdd1daa --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/iostreams/console.go @@ -0,0 +1,11 @@ +//go:build !windows +// +build !windows + +package iostreams + +import "os" + +func hasAlternateScreenBuffer(hasTrueColor bool) bool { + // on non-Windows, we just assume that alternate screen buffer is supported in most cases + return os.Getenv("TERM") != "dumb" +} diff --git a/vendor/github.com/cli/cli/v2/pkg/iostreams/console_windows.go b/vendor/github.com/cli/cli/v2/pkg/iostreams/console_windows.go new file mode 100644 index 000000000..ee0238e49 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/iostreams/console_windows.go @@ -0,0 +1,10 @@ +//go:build windows +// +build windows + +package iostreams + +func hasAlternateScreenBuffer(hasTrueColor bool) bool { + // on Windows we just assume that alternate screen buffer is supported if we + // enabled virtual terminal processing, which in turn enables truecolor + return hasTrueColor +} diff --git a/vendor/github.com/cli/cli/v2/pkg/iostreams/epipe_other.go b/vendor/github.com/cli/cli/v2/pkg/iostreams/epipe_other.go new file mode 100644 index 000000000..a8a4e0476 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/iostreams/epipe_other.go @@ -0,0 +1,13 @@ +//go:build !windows +// +build !windows + +package iostreams + +import ( + "errors" + "syscall" +) + +func isEpipeError(err error) bool { + return errors.Is(err, syscall.EPIPE) +} diff --git a/vendor/github.com/cli/cli/v2/pkg/iostreams/epipe_windows.go b/vendor/github.com/cli/cli/v2/pkg/iostreams/epipe_windows.go new file mode 100644 index 000000000..69815cb15 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/iostreams/epipe_windows.go @@ -0,0 +1,11 @@ +package iostreams + +import ( + "errors" + "syscall" +) + +func isEpipeError(err error) bool { + // 232 is Windows error code ERROR_NO_DATA, "The pipe is being closed". + return errors.Is(err, syscall.Errno(232)) +} diff --git a/vendor/github.com/cli/cli/v2/pkg/iostreams/iostreams.go b/vendor/github.com/cli/cli/v2/pkg/iostreams/iostreams.go new file mode 100644 index 000000000..be057c494 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/iostreams/iostreams.go @@ -0,0 +1,535 @@ +package iostreams + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + "os/exec" + "os/signal" + "strings" + "sync" + "time" + + "github.com/briandowns/spinner" + ghTerm "github.com/cli/go-gh/v2/pkg/term" + "github.com/cli/safeexec" + "github.com/google/shlex" + "github.com/mattn/go-colorable" + "github.com/mattn/go-isatty" +) + +const DefaultWidth = 80 + +// ErrClosedPagerPipe is the error returned when writing to a pager that has been closed. +type ErrClosedPagerPipe struct { + error +} + +type fileWriter interface { + io.Writer + Fd() uintptr +} + +type fileReader interface { + io.ReadCloser + Fd() uintptr +} + +type term interface { + IsTerminalOutput() bool + IsColorEnabled() bool + Is256ColorSupported() bool + IsTrueColorSupported() bool + Theme() string + Size() (int, int, error) +} + +type IOStreams struct { + term term + + In fileReader + Out fileWriter + ErrOut fileWriter + + terminalTheme string + + progressIndicatorEnabled bool + progressIndicator *spinner.Spinner + progressIndicatorMu sync.Mutex + + alternateScreenBufferEnabled bool + alternateScreenBufferActive bool + alternateScreenBufferMu sync.Mutex + + stdinTTYOverride bool + stdinIsTTY bool + stdoutTTYOverride bool + stdoutIsTTY bool + stderrTTYOverride bool + stderrIsTTY bool + + colorOverride bool + colorEnabled bool + + pagerCommand string + pagerProcess *os.Process + + neverPrompt bool + + TempFileOverride *os.File +} + +func (s *IOStreams) ColorEnabled() bool { + if s.colorOverride { + return s.colorEnabled + } + return s.term.IsColorEnabled() +} + +func (s *IOStreams) ColorSupport256() bool { + if s.colorOverride { + return s.colorEnabled + } + return s.term.Is256ColorSupported() +} + +func (s *IOStreams) HasTrueColor() bool { + if s.colorOverride { + return s.colorEnabled + } + return s.term.IsTrueColorSupported() +} + +// DetectTerminalTheme is a utility to call before starting the output pager so that the terminal background +// can be reliably detected. +func (s *IOStreams) DetectTerminalTheme() { + if !s.ColorEnabled() || s.pagerProcess != nil { + s.terminalTheme = "none" + return + } + + style := os.Getenv("GLAMOUR_STYLE") + if style != "" && style != "auto" { + // ensure GLAMOUR_STYLE takes precedence over "light" and "dark" themes + s.terminalTheme = "none" + return + } + + s.terminalTheme = s.term.Theme() +} + +// TerminalTheme returns "light", "dark", or "none" depending on the background color of the terminal. +func (s *IOStreams) TerminalTheme() string { + if s.terminalTheme == "" { + s.DetectTerminalTheme() + } + + return s.terminalTheme +} + +func (s *IOStreams) SetColorEnabled(colorEnabled bool) { + s.colorOverride = true + s.colorEnabled = colorEnabled +} + +func (s *IOStreams) SetStdinTTY(isTTY bool) { + s.stdinTTYOverride = true + s.stdinIsTTY = isTTY +} + +func (s *IOStreams) IsStdinTTY() bool { + if s.stdinTTYOverride { + return s.stdinIsTTY + } + if stdin, ok := s.In.(*os.File); ok { + return isTerminal(stdin) + } + return false +} + +func (s *IOStreams) SetStdoutTTY(isTTY bool) { + s.stdoutTTYOverride = true + s.stdoutIsTTY = isTTY +} + +func (s *IOStreams) IsStdoutTTY() bool { + if s.stdoutTTYOverride { + return s.stdoutIsTTY + } + // support GH_FORCE_TTY + if s.term.IsTerminalOutput() { + return true + } + stdout, ok := s.Out.(*os.File) + return ok && isCygwinTerminal(stdout.Fd()) +} + +func (s *IOStreams) SetStderrTTY(isTTY bool) { + s.stderrTTYOverride = true + s.stderrIsTTY = isTTY +} + +func (s *IOStreams) IsStderrTTY() bool { + if s.stderrTTYOverride { + return s.stderrIsTTY + } + if stderr, ok := s.ErrOut.(*os.File); ok { + return isTerminal(stderr) + } + return false +} + +func (s *IOStreams) SetPager(cmd string) { + s.pagerCommand = cmd +} + +func (s *IOStreams) GetPager() string { + return s.pagerCommand +} + +func (s *IOStreams) StartPager() error { + if s.pagerCommand == "" || s.pagerCommand == "cat" || !s.IsStdoutTTY() { + return nil + } + + pagerArgs, err := shlex.Split(s.pagerCommand) + if err != nil { + return err + } + + pagerEnv := os.Environ() + for i := len(pagerEnv) - 1; i >= 0; i-- { + if strings.HasPrefix(pagerEnv[i], "PAGER=") { + pagerEnv = append(pagerEnv[0:i], pagerEnv[i+1:]...) + } + } + if _, ok := os.LookupEnv("LESS"); !ok { + pagerEnv = append(pagerEnv, "LESS=FRX") + } + if _, ok := os.LookupEnv("LV"); !ok { + pagerEnv = append(pagerEnv, "LV=-c") + } + + pagerExe, err := safeexec.LookPath(pagerArgs[0]) + if err != nil { + return err + } + pagerCmd := exec.Command(pagerExe, pagerArgs[1:]...) + pagerCmd.Env = pagerEnv + pagerCmd.Stdout = s.Out + pagerCmd.Stderr = s.ErrOut + pagedOut, err := pagerCmd.StdinPipe() + if err != nil { + return err + } + s.Out = &fdWriteCloser{ + fd: s.Out.Fd(), + WriteCloser: &pagerWriter{pagedOut}, + } + err = pagerCmd.Start() + if err != nil { + return err + } + s.pagerProcess = pagerCmd.Process + return nil +} + +func (s *IOStreams) StopPager() { + if s.pagerProcess == nil { + return + } + + // if a pager was started, we're guaranteed to have a WriteCloser + _ = s.Out.(io.WriteCloser).Close() + _, _ = s.pagerProcess.Wait() + s.pagerProcess = nil +} + +func (s *IOStreams) CanPrompt() bool { + if s.neverPrompt { + return false + } + + return s.IsStdinTTY() && s.IsStdoutTTY() +} + +func (s *IOStreams) GetNeverPrompt() bool { + return s.neverPrompt +} + +func (s *IOStreams) SetNeverPrompt(v bool) { + s.neverPrompt = v +} + +func (s *IOStreams) StartProgressIndicator() { + s.StartProgressIndicatorWithLabel("") +} + +func (s *IOStreams) StartProgressIndicatorWithLabel(label string) { + if !s.progressIndicatorEnabled { + return + } + + s.progressIndicatorMu.Lock() + defer s.progressIndicatorMu.Unlock() + + if s.progressIndicator != nil { + if label == "" { + s.progressIndicator.Prefix = "" + } else { + s.progressIndicator.Prefix = label + " " + } + return + } + + // https://github.com/briandowns/spinner#available-character-sets + dotStyle := spinner.CharSets[11] + sp := spinner.New(dotStyle, 120*time.Millisecond, spinner.WithWriter(s.ErrOut), spinner.WithColor("fgCyan")) + if label != "" { + sp.Prefix = label + " " + } + + sp.Start() + s.progressIndicator = sp +} + +func (s *IOStreams) StopProgressIndicator() { + s.progressIndicatorMu.Lock() + defer s.progressIndicatorMu.Unlock() + if s.progressIndicator == nil { + return + } + s.progressIndicator.Stop() + s.progressIndicator = nil +} + +func (s *IOStreams) RunWithProgress(label string, run func() error) error { + s.StartProgressIndicatorWithLabel(label) + defer s.StopProgressIndicator() + + return run() +} + +func (s *IOStreams) StartAlternateScreenBuffer() { + if s.alternateScreenBufferEnabled { + s.alternateScreenBufferMu.Lock() + defer s.alternateScreenBufferMu.Unlock() + + if _, err := fmt.Fprint(s.Out, "\x1b[?1049h"); err == nil { + s.alternateScreenBufferActive = true + + ch := make(chan os.Signal, 1) + signal.Notify(ch, os.Interrupt) + + go func() { + <-ch + s.StopAlternateScreenBuffer() + + os.Exit(1) + }() + } + } +} + +func (s *IOStreams) StopAlternateScreenBuffer() { + s.alternateScreenBufferMu.Lock() + defer s.alternateScreenBufferMu.Unlock() + + if s.alternateScreenBufferActive { + fmt.Fprint(s.Out, "\x1b[?1049l") + s.alternateScreenBufferActive = false + } +} + +func (s *IOStreams) SetAlternateScreenBufferEnabled(enabled bool) { + s.alternateScreenBufferEnabled = enabled +} + +func (s *IOStreams) RefreshScreen() { + if s.IsStdoutTTY() { + // Move cursor to 0,0 + fmt.Fprint(s.Out, "\x1b[0;0H") + // Clear from cursor to bottom of screen + fmt.Fprint(s.Out, "\x1b[J") + } +} + +// TerminalWidth returns the width of the terminal that controls the process +func (s *IOStreams) TerminalWidth() int { + w, _, err := s.term.Size() + if err == nil && w > 0 { + return w + } + return DefaultWidth +} + +func (s *IOStreams) ColorScheme() *ColorScheme { + return NewColorScheme(s.ColorEnabled(), s.ColorSupport256(), s.HasTrueColor()) +} + +func (s *IOStreams) ReadUserFile(fn string) ([]byte, error) { + var r io.ReadCloser + if fn == "-" { + r = s.In + } else { + var err error + r, err = os.Open(fn) + if err != nil { + return nil, err + } + } + defer r.Close() + return io.ReadAll(r) +} + +func (s *IOStreams) TempFile(dir, pattern string) (*os.File, error) { + if s.TempFileOverride != nil { + return s.TempFileOverride, nil + } + return os.CreateTemp(dir, pattern) +} + +func System() *IOStreams { + terminal := ghTerm.FromEnv() + + var stdout fileWriter = os.Stdout + // On Windows with no virtual terminal processing support, translate ANSI escape + // sequences to console syscalls. + if colorableStdout := colorable.NewColorable(os.Stdout); colorableStdout != os.Stdout { + // Ensure that the file descriptor of the original stdout is preserved. + stdout = &fdWriter{ + fd: os.Stdout.Fd(), + Writer: colorableStdout, + } + } + + var stderr fileWriter = os.Stderr + // On Windows with no virtual terminal processing support, translate ANSI escape + // sequences to console syscalls. + if colorableStderr := colorable.NewColorable(os.Stderr); colorableStderr != os.Stderr { + // Ensure that the file descriptor of the original stderr is preserved. + stderr = &fdWriter{ + fd: os.Stderr.Fd(), + Writer: colorableStderr, + } + } + + io := &IOStreams{ + In: os.Stdin, + Out: stdout, + ErrOut: stderr, + pagerCommand: os.Getenv("PAGER"), + term: &terminal, + } + + stdoutIsTTY := io.IsStdoutTTY() + stderrIsTTY := io.IsStderrTTY() + + if stdoutIsTTY && stderrIsTTY { + io.progressIndicatorEnabled = true + } + + if stdoutIsTTY && hasAlternateScreenBuffer(terminal.IsTrueColorSupported()) { + io.alternateScreenBufferEnabled = true + } + + return io +} + +type fakeTerm struct{} + +func (t fakeTerm) IsTerminalOutput() bool { + return false +} + +func (t fakeTerm) IsColorEnabled() bool { + return false +} + +func (t fakeTerm) Is256ColorSupported() bool { + return false +} + +func (t fakeTerm) IsTrueColorSupported() bool { + return false +} + +func (t fakeTerm) Theme() string { + return "" +} + +func (t fakeTerm) Size() (int, int, error) { + return 80, -1, nil +} + +func Test() (*IOStreams, *bytes.Buffer, *bytes.Buffer, *bytes.Buffer) { + in := &bytes.Buffer{} + out := &bytes.Buffer{} + errOut := &bytes.Buffer{} + io := &IOStreams{ + In: &fdReader{ + fd: 0, + ReadCloser: io.NopCloser(in), + }, + Out: &fdWriter{fd: 1, Writer: out}, + ErrOut: &fdWriter{fd: 2, Writer: errOut}, + term: &fakeTerm{}, + } + io.SetStdinTTY(false) + io.SetStdoutTTY(false) + io.SetStderrTTY(false) + return io, in, out, errOut +} + +func isTerminal(f *os.File) bool { + return ghTerm.IsTerminal(f) || isCygwinTerminal(f.Fd()) +} + +func isCygwinTerminal(fd uintptr) bool { + return isatty.IsCygwinTerminal(fd) +} + +// pagerWriter implements a WriteCloser that wraps all EPIPE errors in an ErrClosedPagerPipe type. +type pagerWriter struct { + io.WriteCloser +} + +func (w *pagerWriter) Write(d []byte) (int, error) { + n, err := w.WriteCloser.Write(d) + if err != nil && (errors.Is(err, io.ErrClosedPipe) || isEpipeError(err)) { + return n, &ErrClosedPagerPipe{err} + } + return n, err +} + +// fdWriter represents a wrapped stdout Writer that preserves the original file descriptor +type fdWriter struct { + io.Writer + fd uintptr +} + +func (w *fdWriter) Fd() uintptr { + return w.fd +} + +// fdWriteCloser represents a wrapped stdout Writer that preserves the original file descriptor +type fdWriteCloser struct { + io.WriteCloser + fd uintptr +} + +func (w *fdWriteCloser) Fd() uintptr { + return w.fd +} + +// fdWriter represents a wrapped stdin ReadCloser that preserves the original file descriptor +type fdReader struct { + io.ReadCloser + fd uintptr +} + +func (r *fdReader) Fd() uintptr { + return r.fd +} diff --git a/vendor/github.com/cli/cli/v2/pkg/jsoncolor/jsoncolor.go b/vendor/github.com/cli/cli/v2/pkg/jsoncolor/jsoncolor.go new file mode 100644 index 000000000..dbe3d9a4b --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/jsoncolor/jsoncolor.go @@ -0,0 +1,113 @@ +package jsoncolor + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "strings" +) + +const ( + colorDelim = "1;38" // bright white + colorKey = "1;34" // bright blue + colorNull = "36" // cyan + colorString = "32" // green + colorBool = "33" // yellow +) + +// Write colorized JSON output parsed from reader +func Write(w io.Writer, r io.Reader, indent string) error { + dec := json.NewDecoder(r) + dec.UseNumber() + + var idx int + var stack []json.Delim + + for { + t, err := dec.Token() + if err == io.EOF { + break + } + if err != nil { + return err + } + + switch tt := t.(type) { + case json.Delim: + switch tt { + case '{', '[': + stack = append(stack, tt) + idx = 0 + fmt.Fprintf(w, "\x1b[%sm%s\x1b[m", colorDelim, tt) + if dec.More() { + fmt.Fprint(w, "\n", strings.Repeat(indent, len(stack))) + } + continue + case '}', ']': + stack = stack[:len(stack)-1] + idx = 0 + fmt.Fprintf(w, "\x1b[%sm%s\x1b[m", colorDelim, tt) + } + default: + b, err := marshalJSON(tt) + if err != nil { + return err + } + + isKey := len(stack) > 0 && stack[len(stack)-1] == '{' && idx%2 == 0 + idx++ + + var color string + if isKey { + color = colorKey + } else if tt == nil { + color = colorNull + } else { + switch t.(type) { + case string: + color = colorString + case bool: + color = colorBool + } + } + + if color == "" { + _, _ = w.Write(b) + } else { + fmt.Fprintf(w, "\x1b[%sm%s\x1b[m", color, b) + } + + if isKey { + fmt.Fprintf(w, "\x1b[%sm:\x1b[m ", colorDelim) + continue + } + } + + if dec.More() { + fmt.Fprintf(w, "\x1b[%sm,\x1b[m\n%s", colorDelim, strings.Repeat(indent, len(stack))) + } else if len(stack) > 0 { + fmt.Fprint(w, "\n", strings.Repeat(indent, len(stack)-1)) + } else { + fmt.Fprint(w, "\n") + } + } + + return nil +} + +// marshalJSON works like json.Marshal but with HTML-escaping disabled +func marshalJSON(v interface{}) ([]byte, error) { + buf := bytes.Buffer{} + enc := json.NewEncoder(&buf) + enc.SetEscapeHTML(false) + if err := enc.Encode(v); err != nil { + return nil, err + } + bb := buf.Bytes() + // omit trailing newline added by json.Encoder + if len(bb) > 0 && bb[len(bb)-1] == '\n' { + return bb[:len(bb)-1], nil + } + return bb, nil +} diff --git a/vendor/github.com/cli/cli/v2/pkg/set/string_set.go b/vendor/github.com/cli/cli/v2/pkg/set/string_set.go new file mode 100644 index 000000000..0bd679a8d --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/set/string_set.go @@ -0,0 +1,84 @@ +package set + +var exists = struct{}{} + +type stringSet struct { + v []string + m map[string]struct{} +} + +func NewStringSet() *stringSet { + s := &stringSet{} + s.m = make(map[string]struct{}) + s.v = []string{} + return s +} + +func (s *stringSet) Add(value string) { + if s.Contains(value) { + return + } + s.m[value] = exists + s.v = append(s.v, value) +} + +func (s *stringSet) AddValues(values []string) { + for _, v := range values { + s.Add(v) + } +} + +func (s *stringSet) Remove(value string) { + if !s.Contains(value) { + return + } + delete(s.m, value) + s.v = sliceWithout(s.v, value) +} + +func sliceWithout(s []string, v string) []string { + idx := -1 + for i, item := range s { + if item == v { + idx = i + break + } + } + if idx < 0 { + return s + } + return append(s[:idx], s[idx+1:]...) +} + +func (s *stringSet) RemoveValues(values []string) { + for _, v := range values { + s.Remove(v) + } +} + +func (s *stringSet) Contains(value string) bool { + _, c := s.m[value] + return c +} + +func (s *stringSet) Len() int { + return len(s.m) +} + +func (s *stringSet) ToSlice() []string { + return s.v +} + +func (s1 *stringSet) Equal(s2 *stringSet) bool { + if s1.Len() != s2.Len() { + return false + } + isEqual := true + for _, v := range s1.v { + if !s2.Contains(v) { + isEqual = false + break + } + } + return isEqual +} diff --git a/vendor/github.com/cli/cli/v2/pkg/ssh/ssh_keys.go b/vendor/github.com/cli/cli/v2/pkg/ssh/ssh_keys.go new file mode 100644 index 000000000..c750b608a --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/ssh/ssh_keys.go @@ -0,0 +1,113 @@ +package ssh + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + + "github.com/cli/cli/v2/internal/config" + "github.com/cli/cli/v2/internal/run" + "github.com/cli/safeexec" +) + +type Context struct { + ConfigDir string + KeygenExe string +} + +type KeyPair struct { + PublicKeyPath string + PrivateKeyPath string +} + +var ErrKeyAlreadyExists = errors.New("SSH key already exists") + +func (c *Context) LocalPublicKeys() ([]string, error) { + sshDir, err := c.sshDir() + if err != nil { + return nil, err + } + + return filepath.Glob(filepath.Join(sshDir, "*.pub")) +} + +func (c *Context) HasKeygen() bool { + _, err := c.findKeygen() + return err == nil +} + +func (c *Context) GenerateSSHKey(keyName string, passphrase string) (*KeyPair, error) { + keygenExe, err := c.findKeygen() + if err != nil { + return nil, err + } + + sshDir, err := c.sshDir() + if err != nil { + return nil, err + } + + err = os.MkdirAll(sshDir, 0700) + if err != nil { + return nil, fmt.Errorf("could not create .ssh directory: %w", err) + } + + keyFile := filepath.Join(sshDir, keyName) + keyPair := KeyPair{ + PublicKeyPath: keyFile + ".pub", + PrivateKeyPath: keyFile, + } + + if _, err := os.Stat(keyFile); err == nil { + // Still return keyPair because the caller might be OK with this - they can check the error with errors.Is(err, ErrKeyAlreadyExists) + return &keyPair, ErrKeyAlreadyExists + } + + if err := os.MkdirAll(filepath.Dir(keyFile), 0711); err != nil { + return nil, err + } + + keygenCmd := exec.Command(keygenExe, "-t", "ed25519", "-C", "", "-N", passphrase, "-f", keyFile) + err = run.PrepareCmd(keygenCmd).Run() + if err != nil { + return nil, err + } + + return &keyPair, nil +} + +func (c *Context) sshDir() (string, error) { + if c.ConfigDir != "" { + return c.ConfigDir, nil + } + dir, err := config.HomeDirPath(".ssh") + if err == nil { + c.ConfigDir = dir + } + return dir, err +} + +func (c *Context) findKeygen() (string, error) { + if c.KeygenExe != "" { + return c.KeygenExe, nil + } + + keygenExe, err := safeexec.LookPath("ssh-keygen") + if err != nil && runtime.GOOS == "windows" { + // We can try and find ssh-keygen in a Git for Windows install + if gitPath, err := safeexec.LookPath("git"); err == nil { + gitKeygen := filepath.Join(filepath.Dir(gitPath), "..", "usr", "bin", "ssh-keygen.exe") + if _, err = os.Stat(gitKeygen); err == nil { + return gitKeygen, nil + } + } + } + + if err == nil { + c.KeygenExe = keygenExe + } + return keygenExe, err +} diff --git a/vendor/github.com/cli/cli/v2/pkg/surveyext/editor.go b/vendor/github.com/cli/cli/v2/pkg/surveyext/editor.go new file mode 100644 index 000000000..420ecc4e5 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/surveyext/editor.go @@ -0,0 +1,170 @@ +package surveyext + +// This file extends survey.Editor to give it more flexible behavior. For more context, read +// https://github.com/cli/cli/issues/70 +// To see what we extended, search through for EXTENDED comments. + +import ( + "os" + "path/filepath" + "runtime" + + "github.com/AlecAivazis/survey/v2" + "github.com/AlecAivazis/survey/v2/terminal" + shellquote "github.com/kballard/go-shellquote" +) + +var ( + bom = []byte{0xef, 0xbb, 0xbf} + defaultEditor = "nano" // EXTENDED to switch from vim as a default editor +) + +func init() { + if g := os.Getenv("GIT_EDITOR"); g != "" { + defaultEditor = g + } else if v := os.Getenv("VISUAL"); v != "" { + defaultEditor = v + } else if e := os.Getenv("EDITOR"); e != "" { + defaultEditor = e + } else if runtime.GOOS == "windows" { + defaultEditor = "notepad" + } +} + +// EXTENDED to enable different prompting behavior +type GhEditor struct { + *survey.Editor + EditorCommand string + BlankAllowed bool + + lookPath func(string) ([]string, []string, error) +} + +// EXTENDED to change prompt text +var EditorQuestionTemplate = ` +{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} +{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}} +{{- color "default+hb"}}{{ .Message }} {{color "reset"}} +{{- if .ShowAnswer}} + {{- color "cyan"}}{{.Answer}}{{color "reset"}}{{"\n"}} +{{- else }} + {{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ .Config.HelpInput }} for help]{{color "reset"}} {{end}} + {{- if and .Default (not .HideDefault)}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}} + {{- color "cyan"}}[(e) to launch {{ .EditorCommand }}{{- if .BlankAllowed }}, enter to skip{{ end }}] {{color "reset"}} +{{- end}}` + +// EXTENDED to pass editor name (to use in prompt) +type EditorTemplateData struct { + survey.Editor + EditorCommand string + BlankAllowed bool + Answer string + ShowAnswer bool + ShowHelp bool + Config *survey.PromptConfig +} + +// EXTENDED to augment prompt text and keypress handling +func (e *GhEditor) prompt(initialValue string, config *survey.PromptConfig) (interface{}, error) { + err := e.Render( + EditorQuestionTemplate, + // EXTENDED to support printing editor in prompt and BlankAllowed + EditorTemplateData{ + Editor: *e.Editor, + BlankAllowed: e.BlankAllowed, + EditorCommand: EditorName(e.EditorCommand), + Config: config, + }, + ) + if err != nil { + return "", err + } + + // start reading runes from the standard in + rr := e.NewRuneReader() + _ = rr.SetTermMode() + defer func() { _ = rr.RestoreTermMode() }() + + cursor := e.NewCursor() + _ = cursor.Hide() + defer func() { + _ = cursor.Show() + }() + + for { + // EXTENDED to handle the e to edit / enter to skip behavior + BlankAllowed + r, _, err := rr.ReadRune() + if err != nil { + return "", err + } + if r == 'e' { + break + } + if r == '\r' || r == '\n' { + if e.BlankAllowed { + return initialValue, nil + } else { + continue + } + } + if r == terminal.KeyInterrupt { + return "", terminal.InterruptErr + } + if r == terminal.KeyEndTransmission { + break + } + if string(r) == config.HelpInput && e.Help != "" { + err = e.Render( + EditorQuestionTemplate, + EditorTemplateData{ + // EXTENDED to support printing editor in prompt, BlankAllowed + Editor: *e.Editor, + BlankAllowed: e.BlankAllowed, + EditorCommand: EditorName(e.EditorCommand), + ShowHelp: true, + Config: config, + }, + ) + if err != nil { + return "", err + } + } + continue + } + + stdio := e.Stdio() + lookPath := e.lookPath + if lookPath == nil { + lookPath = defaultLookPath + } + text, err := edit(e.EditorCommand, e.FileName, initialValue, stdio.In, stdio.Out, stdio.Err, cursor, lookPath) + if err != nil { + return "", err + } + + // check length, return default value on empty + if len(text) == 0 && !e.AppendDefault { + return e.Default, nil + } + + return text, nil +} + +// EXTENDED This is straight copypasta from survey to get our overridden prompt called.; +func (e *GhEditor) Prompt(config *survey.PromptConfig) (interface{}, error) { + initialValue := "" + if e.Default != "" && e.AppendDefault { + initialValue = e.Default + } + return e.prompt(initialValue, config) +} + +func EditorName(editorCommand string) string { + if editorCommand == "" { + editorCommand = defaultEditor + } + if args, err := shellquote.Split(editorCommand); err == nil { + editorCommand = args[0] + } + return filepath.Base(editorCommand) +} diff --git a/vendor/github.com/cli/cli/v2/pkg/surveyext/editor_manual.go b/vendor/github.com/cli/cli/v2/pkg/surveyext/editor_manual.go new file mode 100644 index 000000000..5ea493774 --- /dev/null +++ b/vendor/github.com/cli/cli/v2/pkg/surveyext/editor_manual.go @@ -0,0 +1,110 @@ +package surveyext + +import ( + "bytes" + "io" + "os" + "os/exec" + "runtime" + + "github.com/cli/safeexec" + shellquote "github.com/kballard/go-shellquote" +) + +type showable interface { + Show() error +} + +func Edit(editorCommand, fn, initialValue string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (string, error) { + return edit(editorCommand, fn, initialValue, stdin, stdout, stderr, nil, defaultLookPath) +} + +func defaultLookPath(name string) ([]string, []string, error) { + exe, err := safeexec.LookPath(name) + if err != nil { + return nil, nil, err + } + return []string{exe}, nil, nil +} + +func needsBom() bool { + // The reason why we do this is because notepad.exe on Windows determines the + // encoding of an "empty" text file by the locale, for example, GBK in China, + // while golang string only handles utf8 well. However, a text file with utf8 + // BOM header is not considered "empty" on Windows, and the encoding will then + // be determined utf8 by notepad.exe, instead of GBK or other encodings. + + // This could be enhanced in the future by doing this only when a non-utf8 + // locale is in use, and possibly doing that for any OS, not just windows. + + return runtime.GOOS == "windows" +} + +func edit(editorCommand, fn, initialValue string, stdin io.Reader, stdout io.Writer, stderr io.Writer, cursor showable, lookPath func(string) ([]string, []string, error)) (string, error) { + // prepare the temp file + pattern := fn + if pattern == "" { + pattern = "survey*.txt" + } + f, err := os.CreateTemp("", pattern) + if err != nil { + return "", err + } + defer os.Remove(f.Name()) + + // write utf8 BOM header if necessary for the current platform and/or locale + if needsBom() { + if _, err := f.Write(bom); err != nil { + return "", err + } + } + + // write initial value + if _, err := f.WriteString(initialValue); err != nil { + return "", err + } + + // close the fd to prevent the editor unable to save file + if err := f.Close(); err != nil { + return "", err + } + + if editorCommand == "" { + editorCommand = defaultEditor + } + args, err := shellquote.Split(editorCommand) + if err != nil { + return "", err + } + args = append(args, f.Name()) + + editorExe, env, err := lookPath(args[0]) + if err != nil { + return "", err + } + args = append(editorExe, args[1:]...) + + cmd := exec.Command(args[0], args[1:]...) + cmd.Env = env + cmd.Stdin = stdin + cmd.Stdout = stdout + cmd.Stderr = stderr + + if cursor != nil { + _ = cursor.Show() + } + + // open the editor + if err := cmd.Run(); err != nil { + return "", err + } + + // raw is a BOM-unstripped UTF8 byte slice + raw, err := os.ReadFile(f.Name()) + if err != nil { + return "", err + } + + // strip BOM header + return string(bytes.TrimPrefix(raw, bom)), nil +} diff --git a/vendor/github.com/cli/cli/v2/utils/utils.go b/vendor/github.com/cli/cli/v2/utils/utils.go new file mode 100644 index 000000000..58894ba4f --- /dev/null +++ b/vendor/github.com/cli/cli/v2/utils/utils.go @@ -0,0 +1,37 @@ +package utils + +import ( + "fmt" + "os" + + "golang.org/x/term" +) + +func IsDebugEnabled() (bool, string) { + debugValue, isDebugSet := os.LookupEnv("GH_DEBUG") + legacyDebugValue := os.Getenv("DEBUG") + + if !isDebugSet { + switch legacyDebugValue { + case "true", "1", "yes", "api": + return true, legacyDebugValue + default: + return false, legacyDebugValue + } + } + + switch debugValue { + case "false", "0", "no", "": + return false, debugValue + default: + return true, debugValue + } +} + +var TerminalSize = func(w interface{}) (int, int, error) { + if f, isFile := w.(*os.File); isFile { + return term.GetSize(int(f.Fd())) + } + + return 0, 0, fmt.Errorf("%v is not a file", w) +} diff --git a/vendor/github.com/cli/go-gh/v2/LICENSE b/vendor/github.com/cli/go-gh/v2/LICENSE new file mode 100644 index 000000000..af732f027 --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 GitHub Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/cli/go-gh/v2/internal/git/git.go b/vendor/github.com/cli/go-gh/v2/internal/git/git.go new file mode 100644 index 000000000..fd79f8da5 --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/internal/git/git.go @@ -0,0 +1,37 @@ +package git + +import ( + "bytes" + "fmt" + "os/exec" + + "github.com/cli/safeexec" +) + +func Exec(args ...string) (stdOut, stdErr bytes.Buffer, err error) { + path, err := path() + if err != nil { + err = fmt.Errorf("could not find git executable in PATH. error: %w", err) + return + } + return run(path, nil, args...) +} + +func path() (string, error) { + return safeexec.LookPath("git") +} + +func run(path string, env []string, args ...string) (stdOut, stdErr bytes.Buffer, err error) { + cmd := exec.Command(path, args...) + cmd.Stdout = &stdOut + cmd.Stderr = &stdErr + if env != nil { + cmd.Env = env + } + err = cmd.Run() + if err != nil { + err = fmt.Errorf("failed to run git: %s. error: %w", stdErr.String(), err) + return + } + return +} diff --git a/vendor/github.com/cli/go-gh/v2/internal/git/remote.go b/vendor/github.com/cli/go-gh/v2/internal/git/remote.go new file mode 100644 index 000000000..917440b8c --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/internal/git/remote.go @@ -0,0 +1,154 @@ +package git + +import ( + "net/url" + "regexp" + "sort" + "strings" +) + +var remoteRE = regexp.MustCompile(`(.+)\s+(.+)\s+\((push|fetch)\)`) + +type RemoteSet []*Remote + +type Remote struct { + Name string + FetchURL *url.URL + PushURL *url.URL + Resolved string + Host string + Owner string + Repo string +} + +func (r RemoteSet) Len() int { return len(r) } +func (r RemoteSet) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r RemoteSet) Less(i, j int) bool { + return remoteNameSortScore(r[i].Name) > remoteNameSortScore(r[j].Name) +} + +func remoteNameSortScore(name string) int { + switch strings.ToLower(name) { + case "upstream": + return 3 + case "github": + return 2 + case "origin": + return 1 + default: + return 0 + } +} + +func Remotes() (RemoteSet, error) { + list, err := listRemotes() + if err != nil { + return nil, err + } + remotes := parseRemotes(list) + setResolvedRemotes(remotes) + sort.Sort(remotes) + return remotes, nil +} + +// Filter remotes by given hostnames, maintains original order. +func (rs RemoteSet) FilterByHosts(hosts []string) RemoteSet { + filtered := make(RemoteSet, 0) + for _, remote := range rs { + for _, host := range hosts { + if strings.EqualFold(remote.Host, host) { + filtered = append(filtered, remote) + break + } + } + } + return filtered +} + +func listRemotes() ([]string, error) { + stdOut, _, err := Exec("remote", "-v") + if err != nil { + return nil, err + } + return toLines(stdOut.String()), nil +} + +func parseRemotes(gitRemotes []string) RemoteSet { + remotes := RemoteSet{} + for _, r := range gitRemotes { + match := remoteRE.FindStringSubmatch(r) + if match == nil { + continue + } + name := strings.TrimSpace(match[1]) + urlStr := strings.TrimSpace(match[2]) + urlType := strings.TrimSpace(match[3]) + + url, err := ParseURL(urlStr) + if err != nil { + continue + } + host, owner, repo, _ := RepoInfoFromURL(url) + + var rem *Remote + if len(remotes) > 0 { + rem = remotes[len(remotes)-1] + if name != rem.Name { + rem = nil + } + } + if rem == nil { + rem = &Remote{Name: name} + remotes = append(remotes, rem) + } + + switch urlType { + case "fetch": + rem.FetchURL = url + rem.Host = host + rem.Owner = owner + rem.Repo = repo + case "push": + rem.PushURL = url + if rem.Host == "" { + rem.Host = host + } + if rem.Owner == "" { + rem.Owner = owner + } + if rem.Repo == "" { + rem.Repo = repo + } + } + } + return remotes +} + +func setResolvedRemotes(remotes RemoteSet) { + stdOut, _, err := Exec("config", "--get-regexp", `^remote\..*\.gh-resolved$`) + if err != nil { + return + } + for _, l := range toLines(stdOut.String()) { + parts := strings.SplitN(l, " ", 2) + if len(parts) < 2 { + continue + } + rp := strings.SplitN(parts[0], ".", 3) + if len(rp) < 2 { + continue + } + name := rp[1] + for _, r := range remotes { + if r.Name == name { + r.Resolved = parts[1] + break + } + } + } +} + +func toLines(output string) []string { + lines := strings.TrimSuffix(output, "\n") + return strings.Split(lines, "\n") +} diff --git a/vendor/github.com/cli/go-gh/v2/internal/git/url.go b/vendor/github.com/cli/go-gh/v2/internal/git/url.go new file mode 100644 index 000000000..8e503c6f9 --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/internal/git/url.go @@ -0,0 +1,83 @@ +package git + +import ( + "fmt" + "net/url" + "strings" +) + +func IsURL(u string) bool { + return strings.HasPrefix(u, "git@") || isSupportedProtocol(u) +} + +func isSupportedProtocol(u string) bool { + return strings.HasPrefix(u, "ssh:") || + strings.HasPrefix(u, "git+ssh:") || + strings.HasPrefix(u, "git:") || + strings.HasPrefix(u, "http:") || + strings.HasPrefix(u, "git+https:") || + strings.HasPrefix(u, "https:") +} + +func isPossibleProtocol(u string) bool { + return isSupportedProtocol(u) || + strings.HasPrefix(u, "ftp:") || + strings.HasPrefix(u, "ftps:") || + strings.HasPrefix(u, "file:") +} + +// ParseURL normalizes git remote urls. +func ParseURL(rawURL string) (u *url.URL, err error) { + if !isPossibleProtocol(rawURL) && + strings.ContainsRune(rawURL, ':') && + // Not a Windows path. + !strings.ContainsRune(rawURL, '\\') { + // Support scp-like syntax for ssh protocol. + rawURL = "ssh://" + strings.Replace(rawURL, ":", "/", 1) + } + + u, err = url.Parse(rawURL) + if err != nil { + return + } + + if u.Scheme == "git+ssh" { + u.Scheme = "ssh" + } + + if u.Scheme == "git+https" { + u.Scheme = "https" + } + + if u.Scheme != "ssh" { + return + } + + if strings.HasPrefix(u.Path, "//") { + u.Path = strings.TrimPrefix(u.Path, "/") + } + + if idx := strings.Index(u.Host, ":"); idx >= 0 { + u.Host = u.Host[0:idx] + } + + return +} + +// Extract GitHub repository information from a git remote URL. +func RepoInfoFromURL(u *url.URL) (host string, owner string, name string, err error) { + if u.Hostname() == "" { + return "", "", "", fmt.Errorf("no hostname detected") + } + + parts := strings.SplitN(strings.Trim(u.Path, "/"), "/", 3) + if len(parts) != 2 { + return "", "", "", fmt.Errorf("invalid path: %s", u.Path) + } + + return normalizeHostname(u.Hostname()), parts[0], strings.TrimSuffix(parts[1], ".git"), nil +} + +func normalizeHostname(h string) string { + return strings.ToLower(strings.TrimPrefix(h, "www.")) +} diff --git a/vendor/github.com/cli/go-gh/v2/internal/set/string_set.go b/vendor/github.com/cli/go-gh/v2/internal/set/string_set.go new file mode 100644 index 000000000..8be4492f1 --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/internal/set/string_set.go @@ -0,0 +1,70 @@ +package set + +var exists = struct{}{} + +type stringSet struct { + v []string + m map[string]struct{} +} + +func NewStringSet() *stringSet { + s := &stringSet{} + s.m = make(map[string]struct{}) + s.v = []string{} + return s +} + +func (s *stringSet) Add(value string) { + if s.Contains(value) { + return + } + s.m[value] = exists + s.v = append(s.v, value) +} + +func (s *stringSet) AddValues(values []string) { + for _, v := range values { + s.Add(v) + } +} + +func (s *stringSet) Remove(value string) { + if !s.Contains(value) { + return + } + delete(s.m, value) + s.v = sliceWithout(s.v, value) +} + +func sliceWithout(s []string, v string) []string { + idx := -1 + for i, item := range s { + if item == v { + idx = i + break + } + } + if idx < 0 { + return s + } + return append(s[:idx], s[idx+1:]...) +} + +func (s *stringSet) RemoveValues(values []string) { + for _, v := range values { + s.Remove(v) + } +} + +func (s *stringSet) Contains(value string) bool { + _, c := s.m[value] + return c +} + +func (s *stringSet) Len() int { + return len(s.m) +} + +func (s *stringSet) ToSlice() []string { + return s.v +} diff --git a/vendor/github.com/cli/go-gh/v2/internal/yamlmap/yaml_map.go b/vendor/github.com/cli/go-gh/v2/internal/yamlmap/yaml_map.go new file mode 100644 index 000000000..78d09911b --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/internal/yamlmap/yaml_map.go @@ -0,0 +1,214 @@ +// Package yamlmap is a wrapper of gopkg.in/yaml.v3 for interacting +// with yaml data as if it were a map. +package yamlmap + +import ( + "errors" + + "gopkg.in/yaml.v3" +) + +const ( + modified = "modifed" +) + +type Map struct { + *yaml.Node +} + +var ErrNotFound = errors.New("not found") +var ErrInvalidYaml = errors.New("invalid yaml") +var ErrInvalidFormat = errors.New("invalid format") + +func StringValue(value string) *Map { + return &Map{&yaml.Node{ + Kind: yaml.ScalarNode, + Tag: "!!str", + Value: value, + }} +} + +func MapValue() *Map { + return &Map{&yaml.Node{ + Kind: yaml.MappingNode, + Tag: "!!map", + }} +} + +func NullValue() *Map { + return &Map{&yaml.Node{ + Kind: yaml.ScalarNode, + Tag: "!!null", + }} +} + +func Unmarshal(data []byte) (*Map, error) { + var root yaml.Node + err := yaml.Unmarshal(data, &root) + if err != nil { + return nil, ErrInvalidYaml + } + if len(root.Content) == 0 { + return MapValue(), nil + } + if root.Content[0].Kind != yaml.MappingNode { + return nil, ErrInvalidFormat + } + return &Map{root.Content[0]}, nil +} + +func Marshal(m *Map) ([]byte, error) { + return yaml.Marshal(m.Node) +} + +func (m *Map) AddEntry(key string, value *Map) { + keyNode := &yaml.Node{ + Kind: yaml.ScalarNode, + Tag: "!!str", + Value: key, + } + m.Content = append(m.Content, keyNode, value.Node) + m.SetModified() +} + +func (m *Map) Empty() bool { + return m.Content == nil || len(m.Content) == 0 +} + +func (m *Map) FindEntry(key string) (*Map, error) { + // Note: The content slice of a yamlMap looks like [key1, value1, key2, value2, ...]. + // When iterating over the content slice we only want to compare the keys of the yamlMap. + for i, v := range m.Content { + if i%2 != 0 { + continue + } + if v.Value == key { + if i+1 < len(m.Content) { + return &Map{m.Content[i+1]}, nil + } + } + } + return nil, ErrNotFound +} + +func (m *Map) Keys() []string { + // Note: The content slice of a yamlMap looks like [key1, value1, key2, value2, ...]. + // When iterating over the content slice we only want to select the keys of the yamlMap. + keys := []string{} + for i, v := range m.Content { + if i%2 != 0 { + continue + } + keys = append(keys, v.Value) + } + return keys +} + +func (m *Map) RemoveEntry(key string) error { + // Note: The content slice of a yamlMap looks like [key1, value1, key2, value2, ...]. + // When iterating over the content slice we only want to compare the keys of the yamlMap. + // If we find they key to remove, remove the key and its value from the content slice. + found, skipNext := false, false + newContent := []*yaml.Node{} + for i, v := range m.Content { + if skipNext { + skipNext = false + continue + } + if i%2 != 0 || v.Value != key { + newContent = append(newContent, v) + } else { + found = true + skipNext = true + m.SetModified() + } + } + if !found { + return ErrNotFound + } + m.Content = newContent + return nil +} + +func (m *Map) SetEntry(key string, value *Map) { + // Note: The content slice of a yamlMap looks like [key1, value1, key2, value2, ...]. + // When iterating over the content slice we only want to compare the keys of the yamlMap. + // If we find they key to set, set the next item in the content slice to the new value. + m.SetModified() + for i, v := range m.Content { + if i%2 != 0 || v.Value != key { + continue + } + if v.Value == key { + if i+1 < len(m.Content) { + m.Content[i+1] = value.Node + return + } + } + } + m.AddEntry(key, value) +} + +// Note: This is a hack to introduce the concept of modified/unmodified +// on top of gopkg.in/yaml.v3. This works by setting the Value property +// of a MappingNode to a specific value and then later checking if the +// node's Value property is that specific value. When a MappingNode gets +// output as a string the Value property is not used, thus changing it +// has no impact for our purposes. +func (m *Map) SetModified() { + // Can not mark a non-mapping node as modified + if m.Node.Kind != yaml.MappingNode && m.Node.Tag == "!!null" { + m.Node.Kind = yaml.MappingNode + m.Node.Tag = "!!map" + } + if m.Node.Kind == yaml.MappingNode { + m.Node.Value = modified + } +} + +// Traverse map using BFS to set all nodes as unmodified. +func (m *Map) SetUnmodified() { + i := 0 + queue := []*yaml.Node{m.Node} + for { + if i > (len(queue) - 1) { + break + } + q := queue[i] + i = i + 1 + if q.Kind != yaml.MappingNode { + continue + } + q.Value = "" + queue = append(queue, q.Content...) + } +} + +// Traverse map using BFS to searach for any nodes that have been modified. +func (m *Map) IsModified() bool { + i := 0 + queue := []*yaml.Node{m.Node} + for { + if i > (len(queue) - 1) { + break + } + q := queue[i] + i = i + 1 + if q.Kind != yaml.MappingNode { + continue + } + if q.Value == modified { + return true + } + queue = append(queue, q.Content...) + } + return false +} + +func (m *Map) String() string { + data, err := Marshal(m) + if err != nil { + return "" + } + return string(data) +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/api/cache.go b/vendor/github.com/cli/go-gh/v2/pkg/api/cache.go new file mode 100644 index 000000000..7085d0525 --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/api/cache.go @@ -0,0 +1,215 @@ +package api + +import ( + "bufio" + "bytes" + "crypto/sha256" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + "time" +) + +type cache struct { + dir string + ttl time.Duration +} + +type cacheRoundTripper struct { + fs fileStorage + rt http.RoundTripper +} + +type fileStorage struct { + dir string + ttl time.Duration + mu *sync.RWMutex +} + +type readCloser struct { + io.Reader + io.Closer +} + +func isCacheableRequest(req *http.Request) bool { + if strings.EqualFold(req.Method, "GET") || strings.EqualFold(req.Method, "HEAD") { + return true + } + + if strings.EqualFold(req.Method, "POST") && (req.URL.Path == "/graphql" || req.URL.Path == "/api/graphql") { + return true + } + + return false +} + +func isCacheableResponse(res *http.Response) bool { + return res.StatusCode < 500 && res.StatusCode != 403 +} + +func cacheKey(req *http.Request) (string, error) { + h := sha256.New() + fmt.Fprintf(h, "%s:", req.Method) + fmt.Fprintf(h, "%s:", req.URL.String()) + fmt.Fprintf(h, "%s:", req.Header.Get("Accept")) + fmt.Fprintf(h, "%s:", req.Header.Get("Authorization")) + + if req.Body != nil { + var bodyCopy io.ReadCloser + req.Body, bodyCopy = copyStream(req.Body) + defer bodyCopy.Close() + if _, err := io.Copy(h, bodyCopy); err != nil { + return "", err + } + } + + digest := h.Sum(nil) + return fmt.Sprintf("%x", digest), nil +} + +func (c cache) RoundTripper(rt http.RoundTripper) http.RoundTripper { + fs := fileStorage{ + dir: c.dir, + ttl: c.ttl, + mu: &sync.RWMutex{}, + } + return cacheRoundTripper{fs: fs, rt: rt} +} + +func (crt cacheRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + reqDir, reqTTL := requestCacheOptions(req) + + if crt.fs.ttl == 0 && reqTTL == 0 { + return crt.rt.RoundTrip(req) + } + + if !isCacheableRequest(req) { + return crt.rt.RoundTrip(req) + } + + origDir := crt.fs.dir + if reqDir != "" { + crt.fs.dir = reqDir + } + origTTL := crt.fs.ttl + if reqTTL != 0 { + crt.fs.ttl = reqTTL + } + + key, keyErr := cacheKey(req) + if keyErr == nil { + if res, err := crt.fs.read(key); err == nil { + res.Request = req + return res, nil + } + } + + res, err := crt.rt.RoundTrip(req) + if err == nil && keyErr == nil && isCacheableResponse(res) { + _ = crt.fs.store(key, res) + } + + crt.fs.dir = origDir + crt.fs.ttl = origTTL + + return res, err +} + +// Allow an individual request to override cache options. +func requestCacheOptions(req *http.Request) (string, time.Duration) { + var dur time.Duration + dir := req.Header.Get("X-GH-CACHE-DIR") + ttl := req.Header.Get("X-GH-CACHE-TTL") + if ttl != "" { + dur, _ = time.ParseDuration(ttl) + } + return dir, dur +} + +func (fs *fileStorage) filePath(key string) string { + if len(key) >= 6 { + return filepath.Join(fs.dir, key[0:2], key[2:4], key[4:]) + } + return filepath.Join(fs.dir, key) +} + +func (fs *fileStorage) read(key string) (*http.Response, error) { + cacheFile := fs.filePath(key) + + fs.mu.RLock() + defer fs.mu.RUnlock() + + f, err := os.Open(cacheFile) + if err != nil { + return nil, err + } + defer f.Close() + + stat, err := f.Stat() + if err != nil { + return nil, err + } + + age := time.Since(stat.ModTime()) + if age > fs.ttl { + return nil, errors.New("cache expired") + } + + body := &bytes.Buffer{} + _, err = io.Copy(body, f) + if err != nil { + return nil, err + } + + res, err := http.ReadResponse(bufio.NewReader(body), nil) + return res, err +} + +func (fs *fileStorage) store(key string, res *http.Response) (storeErr error) { + cacheFile := fs.filePath(key) + + fs.mu.Lock() + defer fs.mu.Unlock() + + if storeErr = os.MkdirAll(filepath.Dir(cacheFile), 0755); storeErr != nil { + return + } + + var f *os.File + if f, storeErr = os.OpenFile(cacheFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600); storeErr != nil { + return + } + + defer func() { + if err := f.Close(); storeErr == nil && err != nil { + storeErr = err + } + }() + + var origBody io.ReadCloser + if res.Body != nil { + origBody, res.Body = copyStream(res.Body) + defer res.Body.Close() + } + + storeErr = res.Write(f) + if origBody != nil { + res.Body = origBody + } + + return +} + +func copyStream(r io.ReadCloser) (io.ReadCloser, io.ReadCloser) { + b := &bytes.Buffer{} + nr := io.TeeReader(r, b) + return io.NopCloser(b), &readCloser{ + Reader: nr, + Closer: r, + } +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/api/client_options.go b/vendor/github.com/cli/go-gh/v2/pkg/api/client_options.go new file mode 100644 index 000000000..3464aacc0 --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/api/client_options.go @@ -0,0 +1,106 @@ +// Package api is a set of types for interacting with the GitHub API. +package api + +import ( + "fmt" + "io" + "net/http" + "time" + + "github.com/cli/go-gh/v2/pkg/auth" + "github.com/cli/go-gh/v2/pkg/config" +) + +// ClientOptions holds available options to configure API clients. +type ClientOptions struct { + // AuthToken is the authorization token that will be used + // to authenticate against API endpoints. + AuthToken string + + // CacheDir is the directory to use for cached API requests. + // Default is the same directory that gh uses for caching. + CacheDir string + + // CacheTTL is the time that cached API requests are valid for. + // Default is 24 hours. + CacheTTL time.Duration + + // EnableCache specifies if API requests will be cached or not. + // Default is no caching. + EnableCache bool + + // Headers are the headers that will be sent with every API request. + // Default headers set are Accept, Content-Type, Time-Zone, and User-Agent. + // Default headers will be overridden by keys specified in Headers. + Headers map[string]string + + // Host is the default host that API requests will be sent to. + Host string + + // Log specifies a writer to write API request logs to. Default is to respect the GH_DEBUG environment + // variable, and no logging otherwise. + Log io.Writer + + // LogIgnoreEnv disables respecting the GH_DEBUG environment variable. This can be useful in test mode + // or when the extension already offers its own controls for logging to the user. + LogIgnoreEnv bool + + // LogColorize enables colorized logging to Log for display in a terminal. + // Default is no coloring. + LogColorize bool + + // LogVerboseHTTP enables logging HTTP headers and bodies to Log. + // Default is only logging request URLs and response statuses. + LogVerboseHTTP bool + + // SkipDefaultHeaders disables setting of the default headers. + SkipDefaultHeaders bool + + // Timeout specifies a time limit for each API request. + // Default is no timeout. + Timeout time.Duration + + // Transport specifies the mechanism by which individual API requests are made. + // If both Transport and UnixDomainSocket are specified then Transport takes + // precedence. Due to this behavior any value set for Transport needs to manually + // handle routing to UnixDomainSocket if necessary. Generally, setting Transport + // should be reserved for testing purposes. + // Default is http.DefaultTransport. + Transport http.RoundTripper + + // UnixDomainSocket specifies the Unix domain socket address by which individual + // API requests will be routed. If specifed, this will form the base of the API + // request transport chain. + // Default is no socket address. + UnixDomainSocket string +} + +func optionsNeedResolution(opts ClientOptions) bool { + if opts.Host == "" { + return true + } + if opts.AuthToken == "" { + return true + } + if opts.UnixDomainSocket == "" && opts.Transport == nil { + return true + } + return false +} + +func resolveOptions(opts ClientOptions) (ClientOptions, error) { + cfg, _ := config.Read(nil) + if opts.Host == "" { + opts.Host, _ = auth.DefaultHost() + } + if opts.AuthToken == "" { + opts.AuthToken, _ = auth.TokenForHost(opts.Host) + if opts.AuthToken == "" { + return ClientOptions{}, fmt.Errorf("authentication token not found for host %s", opts.Host) + } + } + if opts.UnixDomainSocket == "" && cfg != nil { + opts.UnixDomainSocket, _ = cfg.Get([]string{"http_unix_socket"}) + } + return opts, nil +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/api/errors.go b/vendor/github.com/cli/go-gh/v2/pkg/api/errors.go new file mode 100644 index 000000000..e8fb93fb1 --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/api/errors.go @@ -0,0 +1,169 @@ +package api + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +// HTTPError represents an error response from the GitHub API. +type HTTPError struct { + Errors []HTTPErrorItem + Headers http.Header + Message string + RequestURL *url.URL + StatusCode int +} + +// HTTPErrorItem stores additional information about an error response +// returned from the GitHub API. +type HTTPErrorItem struct { + Code string + Field string + Message string + Resource string +} + +// Allow HTTPError to satisfy error interface. +func (err *HTTPError) Error() string { + if msgs := strings.SplitN(err.Message, "\n", 2); len(msgs) > 1 { + return fmt.Sprintf("HTTP %d: %s (%s)\n%s", err.StatusCode, msgs[0], err.RequestURL, msgs[1]) + } else if err.Message != "" { + return fmt.Sprintf("HTTP %d: %s (%s)", err.StatusCode, err.Message, err.RequestURL) + } + return fmt.Sprintf("HTTP %d (%s)", err.StatusCode, err.RequestURL) +} + +// GraphQLError represents an error response from GitHub GraphQL API. +type GraphQLError struct { + Errors []GraphQLErrorItem +} + +// GraphQLErrorItem stores additional information about an error response +// returned from the GitHub GraphQL API. +type GraphQLErrorItem struct { + Message string + Locations []struct { + Line int + Column int + } + Path []interface{} + Extensions map[string]interface{} + Type string +} + +// Allow GraphQLError to satisfy error interface. +func (gr *GraphQLError) Error() string { + errorMessages := make([]string, 0, len(gr.Errors)) + for _, e := range gr.Errors { + msg := e.Message + if p := e.pathString(); p != "" { + msg = fmt.Sprintf("%s (%s)", msg, p) + } + errorMessages = append(errorMessages, msg) + } + return fmt.Sprintf("GraphQL: %s", strings.Join(errorMessages, ", ")) +} + +// Match determines if the GraphQLError is about a specific type on a specific path. +// If the path argument ends with a ".", it will match all its subpaths. +func (gr *GraphQLError) Match(expectType, expectPath string) bool { + for _, e := range gr.Errors { + if e.Type != expectType || !matchPath(e.pathString(), expectPath) { + return false + } + } + return true +} + +func (ge GraphQLErrorItem) pathString() string { + var res strings.Builder + for i, v := range ge.Path { + if i > 0 { + res.WriteRune('.') + } + fmt.Fprintf(&res, "%v", v) + } + return res.String() +} + +func matchPath(p, expect string) bool { + if strings.HasSuffix(expect, ".") { + return strings.HasPrefix(p, expect) || p == strings.TrimSuffix(expect, ".") + } + return p == expect +} + +// HandleHTTPError parses a http.Response into a HTTPError. +func HandleHTTPError(resp *http.Response) error { + httpError := &HTTPError{ + Headers: resp.Header, + RequestURL: resp.Request.URL, + StatusCode: resp.StatusCode, + } + + if !jsonTypeRE.MatchString(resp.Header.Get(contentType)) { + httpError.Message = resp.Status + return httpError + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + httpError.Message = err.Error() + return httpError + } + + var parsedBody struct { + Message string `json:"message"` + Errors []json.RawMessage + } + if err := json.Unmarshal(body, &parsedBody); err != nil { + return httpError + } + + var messages []string + if parsedBody.Message != "" { + messages = append(messages, parsedBody.Message) + } + for _, raw := range parsedBody.Errors { + switch raw[0] { + case '"': + var errString string + _ = json.Unmarshal(raw, &errString) + messages = append(messages, errString) + httpError.Errors = append(httpError.Errors, HTTPErrorItem{Message: errString}) + case '{': + var errInfo HTTPErrorItem + _ = json.Unmarshal(raw, &errInfo) + msg := errInfo.Message + if errInfo.Code != "" && errInfo.Code != "custom" { + msg = fmt.Sprintf("%s.%s %s", errInfo.Resource, errInfo.Field, errorCodeToMessage(errInfo.Code)) + } + if msg != "" { + messages = append(messages, msg) + } + httpError.Errors = append(httpError.Errors, errInfo) + } + } + httpError.Message = strings.Join(messages, "\n") + + return httpError +} + +// Convert common error codes to human readable messages +// See https://docs.github.com/en/rest/overview/resources-in-the-rest-api#client-errors for more details. +func errorCodeToMessage(code string) string { + switch code { + case "missing", "missing_field": + return "is missing" + case "invalid", "unprocessable": + return "is invalid" + case "already_exists": + return "already exists" + default: + return code + } +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/api/graphql_client.go b/vendor/github.com/cli/go-gh/v2/pkg/api/graphql_client.go new file mode 100644 index 000000000..405b10f31 --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/api/graphql_client.go @@ -0,0 +1,182 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + + graphql "github.com/cli/shurcooL-graphql" +) + +// GraphQLClient wraps methods for the different types of +// API requests that are supported by the server. +type GraphQLClient struct { + client *graphql.Client + host string + httpClient *http.Client +} + +func DefaultGraphQLClient() (*GraphQLClient, error) { + return NewGraphQLClient(ClientOptions{}) +} + +// GraphQLClient builds a client to send requests to GitHub GraphQL API endpoints. +// As part of the configuration a hostname, auth token, default set of headers, +// and unix domain socket are resolved from the gh environment configuration. +// These behaviors can be overridden using the opts argument. +func NewGraphQLClient(opts ClientOptions) (*GraphQLClient, error) { + if optionsNeedResolution(opts) { + var err error + opts, err = resolveOptions(opts) + if err != nil { + return nil, err + } + } + + httpClient, err := NewHTTPClient(opts) + if err != nil { + return nil, err + } + + endpoint := graphQLEndpoint(opts.Host) + + return &GraphQLClient{ + client: graphql.NewClient(endpoint, httpClient), + host: endpoint, + httpClient: httpClient, + }, nil +} + +// DoWithContext executes a GraphQL query request. +// The response is populated into the response argument. +func (c *GraphQLClient) DoWithContext(ctx context.Context, query string, variables map[string]interface{}, response interface{}) error { + reqBody, err := json.Marshal(map[string]interface{}{"query": query, "variables": variables}) + if err != nil { + return err + } + + req, err := http.NewRequestWithContext(ctx, "POST", c.host, bytes.NewBuffer(reqBody)) + if err != nil { + return err + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + success := resp.StatusCode >= 200 && resp.StatusCode < 300 + if !success { + return HandleHTTPError(resp) + } + + if resp.StatusCode == http.StatusNoContent { + return nil + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + gr := graphQLResponse{Data: response} + err = json.Unmarshal(body, &gr) + if err != nil { + return err + } + + if len(gr.Errors) > 0 { + return &GraphQLError{Errors: gr.Errors} + } + + return nil +} + +// Do wraps DoWithContext using context.Background. +func (c *GraphQLClient) Do(query string, variables map[string]interface{}, response interface{}) error { + return c.DoWithContext(context.Background(), query, variables, response) +} + +// MutateWithContext executes a GraphQL mutation request. +// The mutation string is derived from the mutation argument, and the +// response is populated into it. +// The mutation argument should be a pointer to struct that corresponds +// to the GitHub GraphQL schema. +// Provided input will be set as a variable named input. +func (c *GraphQLClient) MutateWithContext(ctx context.Context, name string, m interface{}, variables map[string]interface{}) error { + err := c.client.MutateNamed(ctx, name, m, variables) + var graphQLErrs graphql.Errors + if err != nil && errors.As(err, &graphQLErrs) { + items := make([]GraphQLErrorItem, len(graphQLErrs)) + for i, e := range graphQLErrs { + items[i] = GraphQLErrorItem{ + Message: e.Message, + Locations: e.Locations, + Path: e.Path, + Extensions: e.Extensions, + Type: e.Type, + } + } + err = &GraphQLError{items} + } + return err +} + +// Mutate wraps MutateWithContext using context.Background. +func (c *GraphQLClient) Mutate(name string, m interface{}, variables map[string]interface{}) error { + return c.MutateWithContext(context.Background(), name, m, variables) +} + +// QueryWithContext executes a GraphQL query request, +// The query string is derived from the query argument, and the +// response is populated into it. +// The query argument should be a pointer to struct that corresponds +// to the GitHub GraphQL schema. +func (c *GraphQLClient) QueryWithContext(ctx context.Context, name string, q interface{}, variables map[string]interface{}) error { + err := c.client.QueryNamed(ctx, name, q, variables) + var graphQLErrs graphql.Errors + if err != nil && errors.As(err, &graphQLErrs) { + items := make([]GraphQLErrorItem, len(graphQLErrs)) + for i, e := range graphQLErrs { + items[i] = GraphQLErrorItem{ + Message: e.Message, + Locations: e.Locations, + Path: e.Path, + Extensions: e.Extensions, + Type: e.Type, + } + } + err = &GraphQLError{items} + } + return err +} + +// Query wraps QueryWithContext using context.Background. +func (c *GraphQLClient) Query(name string, q interface{}, variables map[string]interface{}) error { + return c.QueryWithContext(context.Background(), name, q, variables) +} + +type graphQLResponse struct { + Data interface{} + Errors []GraphQLErrorItem +} + +func graphQLEndpoint(host string) string { + if isGarage(host) { + return fmt.Sprintf("https://%s/api/graphql", host) + } + host = normalizeHostname(host) + if isEnterprise(host) { + return fmt.Sprintf("https://%s/api/graphql", host) + } + if strings.EqualFold(host, localhost) { + return fmt.Sprintf("http://api.%s/graphql", host) + } + return fmt.Sprintf("https://api.%s/graphql", host) +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/api/http_client.go b/vendor/github.com/cli/go-gh/v2/pkg/api/http_client.go new file mode 100644 index 000000000..589de91ae --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/api/http_client.go @@ -0,0 +1,259 @@ +package api + +import ( + "fmt" + "io" + "net" + "net/http" + "os" + "regexp" + "runtime/debug" + "strings" + "time" + + "github.com/cli/go-gh/v2/pkg/asciisanitizer" + "github.com/cli/go-gh/v2/pkg/config" + "github.com/cli/go-gh/v2/pkg/term" + "github.com/henvic/httpretty" + "github.com/thlib/go-timezone-local/tzlocal" + "golang.org/x/text/transform" +) + +const ( + accept = "Accept" + authorization = "Authorization" + contentType = "Content-Type" + github = "github.com" + jsonContentType = "application/json; charset=utf-8" + localhost = "github.localhost" + modulePath = "github.com/cli/go-gh" + timeZone = "Time-Zone" + userAgent = "User-Agent" +) + +var jsonTypeRE = regexp.MustCompile(`[/+]json($|;)`) + +func DefaultHTTPClient() (*http.Client, error) { + return NewHTTPClient(ClientOptions{}) +} + +// HTTPClient builds a client that can be passed to another library. +// As part of the configuration a hostname, auth token, default set of headers, +// and unix domain socket are resolved from the gh environment configuration. +// These behaviors can be overridden using the opts argument. In this instance +// providing opts.Host will not change the destination of your request as it is +// the responsibility of the consumer to configure this. However, if opts.Host +// does not match the request host, the auth token will not be added to the headers. +// This is to protect against the case where tokens could be sent to an arbitrary +// host. +func NewHTTPClient(opts ClientOptions) (*http.Client, error) { + if optionsNeedResolution(opts) { + var err error + opts, err = resolveOptions(opts) + if err != nil { + return nil, err + } + } + + transport := http.DefaultTransport + + if opts.UnixDomainSocket != "" { + transport = newUnixDomainSocketRoundTripper(opts.UnixDomainSocket) + } + + if opts.Transport != nil { + transport = opts.Transport + } + + transport = newSanitizerRoundTripper(transport) + + if opts.CacheDir == "" { + opts.CacheDir = config.CacheDir() + } + if opts.EnableCache && opts.CacheTTL == 0 { + opts.CacheTTL = time.Hour * 24 + } + c := cache{dir: opts.CacheDir, ttl: opts.CacheTTL} + transport = c.RoundTripper(transport) + + if opts.Log == nil && !opts.LogIgnoreEnv { + ghDebug := os.Getenv("GH_DEBUG") + switch ghDebug { + case "", "0", "false", "no": + // no logging + default: + opts.Log = os.Stderr + opts.LogColorize = !term.IsColorDisabled() && term.IsTerminal(os.Stderr) + opts.LogVerboseHTTP = strings.Contains(ghDebug, "api") + } + } + + if opts.Log != nil { + logger := &httpretty.Logger{ + Time: true, + TLS: false, + Colors: opts.LogColorize, + RequestHeader: opts.LogVerboseHTTP, + RequestBody: opts.LogVerboseHTTP, + ResponseHeader: opts.LogVerboseHTTP, + ResponseBody: opts.LogVerboseHTTP, + Formatters: []httpretty.Formatter{&jsonFormatter{colorize: opts.LogColorize}}, + MaxResponseBody: 100000, + } + logger.SetOutput(opts.Log) + logger.SetBodyFilter(func(h http.Header) (skip bool, err error) { + return !inspectableMIMEType(h.Get(contentType)), nil + }) + transport = logger.RoundTripper(transport) + } + + if opts.Headers == nil { + opts.Headers = map[string]string{} + } + if !opts.SkipDefaultHeaders { + resolveHeaders(opts.Headers) + } + transport = newHeaderRoundTripper(opts.Host, opts.AuthToken, opts.Headers, transport) + + return &http.Client{Transport: transport, Timeout: opts.Timeout}, nil +} + +func inspectableMIMEType(t string) bool { + return strings.HasPrefix(t, "text/") || + strings.HasPrefix(t, "application/x-www-form-urlencoded") || + jsonTypeRE.MatchString(t) +} + +func isSameDomain(requestHost, domain string) bool { + requestHost = strings.ToLower(requestHost) + domain = strings.ToLower(domain) + return (requestHost == domain) || strings.HasSuffix(requestHost, "."+domain) +} + +func isGarage(host string) bool { + return strings.EqualFold(host, "garage.github.com") +} + +func isEnterprise(host string) bool { + return host != github && host != localhost +} + +func normalizeHostname(hostname string) string { + hostname = strings.ToLower(hostname) + if strings.HasSuffix(hostname, "."+github) { + return github + } + if strings.HasSuffix(hostname, "."+localhost) { + return localhost + } + return hostname +} + +type headerRoundTripper struct { + headers map[string]string + host string + rt http.RoundTripper +} + +func resolveHeaders(headers map[string]string) { + if _, ok := headers[contentType]; !ok { + headers[contentType] = jsonContentType + } + if _, ok := headers[userAgent]; !ok { + headers[userAgent] = "go-gh" + info, ok := debug.ReadBuildInfo() + if ok { + for _, dep := range info.Deps { + if dep.Path == modulePath { + headers[userAgent] += fmt.Sprintf(" %s", dep.Version) + break + } + } + } + } + if _, ok := headers[timeZone]; !ok { + tz := currentTimeZone() + if tz != "" { + headers[timeZone] = tz + } + } + if _, ok := headers[accept]; !ok { + // Preview for PullRequest.mergeStateStatus. + a := "application/vnd.github.merge-info-preview+json" + // Preview for visibility when RESTing repos into an org. + a += ", application/vnd.github.nebula-preview" + headers[accept] = a + } +} + +func newHeaderRoundTripper(host string, authToken string, headers map[string]string, rt http.RoundTripper) http.RoundTripper { + if _, ok := headers[authorization]; !ok && authToken != "" { + headers[authorization] = fmt.Sprintf("token %s", authToken) + } + if len(headers) == 0 { + return rt + } + return headerRoundTripper{host: host, headers: headers, rt: rt} +} + +func (hrt headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + for k, v := range hrt.headers { + // If the authorization header has been set and the request + // host is not in the same domain that was specified in the ClientOptions + // then do not add the authorization header to the request. + if k == authorization && !isSameDomain(req.URL.Hostname(), hrt.host) { + continue + } + + // If the header is already set in the request, don't overwrite it. + if req.Header.Get(k) == "" { + req.Header.Set(k, v) + } + } + + return hrt.rt.RoundTrip(req) +} + +func newUnixDomainSocketRoundTripper(socketPath string) http.RoundTripper { + dial := func(network, addr string) (net.Conn, error) { + return net.Dial("unix", socketPath) + } + + return &http.Transport{ + Dial: dial, + DialTLS: dial, + DisableKeepAlives: true, + } +} + +type sanitizerRoundTripper struct { + rt http.RoundTripper +} + +func newSanitizerRoundTripper(rt http.RoundTripper) http.RoundTripper { + return sanitizerRoundTripper{rt: rt} +} + +func (srt sanitizerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + resp, err := srt.rt.RoundTrip(req) + if err != nil || !jsonTypeRE.MatchString(resp.Header.Get(contentType)) { + return resp, err + } + sanitizedReadCloser := struct { + io.Reader + io.Closer + }{ + Reader: transform.NewReader(resp.Body, &asciisanitizer.Sanitizer{JSON: true}), + Closer: resp.Body, + } + resp.Body = sanitizedReadCloser + return resp, err +} + +func currentTimeZone() string { + tz, err := tzlocal.RuntimeTZ() + if err != nil { + return "" + } + return tz +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/api/log_formatter.go b/vendor/github.com/cli/go-gh/v2/pkg/api/log_formatter.go new file mode 100644 index 000000000..e3a85f043 --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/api/log_formatter.go @@ -0,0 +1,47 @@ +package api + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "strings" + + "github.com/cli/go-gh/v2/pkg/jsonpretty" +) + +type graphqlBody struct { + Query string `json:"query"` + OperationName string `json:"operationName"` + Variables json.RawMessage `json:"variables"` +} + +// jsonFormatter is a httpretty.Formatter that prettifies JSON payloads and GraphQL queries. +type jsonFormatter struct { + colorize bool +} + +func (f *jsonFormatter) Format(w io.Writer, src []byte) error { + var graphqlQuery graphqlBody + // TODO: find more precise way to detect a GraphQL query from the JSON payload alone + if err := json.Unmarshal(src, &graphqlQuery); err == nil && graphqlQuery.Query != "" && len(graphqlQuery.Variables) > 0 { + colorHighlight := "\x1b[35;1m" + colorReset := "\x1b[m" + if !f.colorize { + colorHighlight = "" + colorReset = "" + } + if _, err := fmt.Fprintf(w, "%sGraphQL query:%s\n%s\n", colorHighlight, colorReset, strings.ReplaceAll(strings.TrimSpace(graphqlQuery.Query), "\t", " ")); err != nil { + return err + } + if _, err := fmt.Fprintf(w, "%sGraphQL variables:%s %s\n", colorHighlight, colorReset, string(graphqlQuery.Variables)); err != nil { + return err + } + return nil + } + return jsonpretty.Format(w, bytes.NewReader(src), " ", f.colorize) +} + +func (f *jsonFormatter) Match(t string) bool { + return jsonTypeRE.MatchString(t) +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/api/rest_client.go b/vendor/github.com/cli/go-gh/v2/pkg/api/rest_client.go new file mode 100644 index 000000000..2d91f707d --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/api/rest_client.go @@ -0,0 +1,170 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + +// RESTClient wraps methods for the different types of +// API requests that are supported by the server. +type RESTClient struct { + client *http.Client + host string +} + +func DefaultRESTClient() (*RESTClient, error) { + return NewRESTClient(ClientOptions{}) +} + +// RESTClient builds a client to send requests to GitHub REST API endpoints. +// As part of the configuration a hostname, auth token, default set of headers, +// and unix domain socket are resolved from the gh environment configuration. +// These behaviors can be overridden using the opts argument. +func NewRESTClient(opts ClientOptions) (*RESTClient, error) { + if optionsNeedResolution(opts) { + var err error + opts, err = resolveOptions(opts) + if err != nil { + return nil, err + } + } + + client, err := NewHTTPClient(opts) + if err != nil { + return nil, err + } + + return &RESTClient{ + client: client, + host: opts.Host, + }, nil +} + +// RequestWithContext issues a request with type specified by method to the +// specified path with the specified body. +// The response is returned rather than being populated +// into a response argument. +func (c *RESTClient) RequestWithContext(ctx context.Context, method string, path string, body io.Reader) (*http.Response, error) { + url := restURL(c.host, path) + req, err := http.NewRequestWithContext(ctx, method, url, body) + if err != nil { + return nil, err + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + + success := resp.StatusCode >= 200 && resp.StatusCode < 300 + if !success { + defer resp.Body.Close() + return nil, HandleHTTPError(resp) + } + + return resp, err +} + +// Request wraps RequestWithContext with context.Background. +func (c *RESTClient) Request(method string, path string, body io.Reader) (*http.Response, error) { + return c.RequestWithContext(context.Background(), method, path, body) +} + +// DoWithContext issues a request with type specified by method to the +// specified path with the specified body. +// The response is populated into the response argument. +func (c *RESTClient) DoWithContext(ctx context.Context, method string, path string, body io.Reader, response interface{}) error { + url := restURL(c.host, path) + req, err := http.NewRequestWithContext(ctx, method, url, body) + if err != nil { + return err + } + + resp, err := c.client.Do(req) + if err != nil { + return err + } + + success := resp.StatusCode >= 200 && resp.StatusCode < 300 + if !success { + defer resp.Body.Close() + return HandleHTTPError(resp) + } + + if resp.StatusCode == http.StatusNoContent { + return nil + } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + err = json.Unmarshal(b, &response) + if err != nil { + return err + } + + return nil +} + +// Do wraps DoWithContext with context.Background. +func (c *RESTClient) Do(method string, path string, body io.Reader, response interface{}) error { + return c.DoWithContext(context.Background(), method, path, body, response) +} + +// Delete issues a DELETE request to the specified path. +// The response is populated into the response argument. +func (c *RESTClient) Delete(path string, resp interface{}) error { + return c.Do(http.MethodDelete, path, nil, resp) +} + +// Get issues a GET request to the specified path. +// The response is populated into the response argument. +func (c *RESTClient) Get(path string, resp interface{}) error { + return c.Do(http.MethodGet, path, nil, resp) +} + +// Patch issues a PATCH request to the specified path with the specified body. +// The response is populated into the response argument. +func (c *RESTClient) Patch(path string, body io.Reader, resp interface{}) error { + return c.Do(http.MethodPatch, path, body, resp) +} + +// Post issues a POST request to the specified path with the specified body. +// The response is populated into the response argument. +func (c *RESTClient) Post(path string, body io.Reader, resp interface{}) error { + return c.Do(http.MethodPost, path, body, resp) +} + +// Put issues a PUT request to the specified path with the specified body. +// The response is populated into the response argument. +func (c *RESTClient) Put(path string, body io.Reader, resp interface{}) error { + return c.Do(http.MethodPut, path, body, resp) +} + +func restURL(hostname string, pathOrURL string) string { + if strings.HasPrefix(pathOrURL, "https://") || strings.HasPrefix(pathOrURL, "http://") { + return pathOrURL + } + return restPrefix(hostname) + pathOrURL +} + +func restPrefix(hostname string) string { + if isGarage(hostname) { + return fmt.Sprintf("https://%s/api/v3/", hostname) + } + hostname = normalizeHostname(hostname) + if isEnterprise(hostname) { + return fmt.Sprintf("https://%s/api/v3/", hostname) + } + if strings.EqualFold(hostname, localhost) { + return fmt.Sprintf("http://api.%s/", hostname) + } + return fmt.Sprintf("https://api.%s/", hostname) +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/asciisanitizer/sanitizer.go b/vendor/github.com/cli/go-gh/v2/pkg/asciisanitizer/sanitizer.go new file mode 100644 index 000000000..0292bec4b --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/asciisanitizer/sanitizer.go @@ -0,0 +1,253 @@ +// Package asciisanitizer implements an ASCII control character sanitizer for UTF-8 strings. +// It will transform ASCII control codes into equivalent inert characters that are safe for display in the terminal. +// Without sanitization these ASCII control characters will be interpreted by the terminal. +// This behaviour can be used maliciously as an attack vector, especially the ASCII control characters \x1B and \x9B. +package asciisanitizer + +import ( + "bytes" + "errors" + "strings" + "unicode" + "unicode/utf8" + + "golang.org/x/text/transform" +) + +// Sanitizer implements transform.Transformer interface. +type Sanitizer struct { + // JSON tells the Sanitizer to replace strings that will be transformed + // into control characters when the string is marshaled to JSON. Set to + // true if the string being sanitized represents JSON formatted data. + JSON bool + addEscape bool +} + +// Transform uses a sliding window algorithm to detect C0 and C1 control characters as they are read and replaces +// them with equivalent inert characters. Bytes that are not part of a control character are not modified. +func (t *Sanitizer) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { + transfer := func(write, read []byte) error { + readLength := len(read) + writeLength := len(write) + if writeLength > len(dst) { + return transform.ErrShortDst + } + copy(dst, write) + nDst += writeLength + dst = dst[writeLength:] + nSrc += readLength + src = src[readLength:] + return nil + } + + for len(src) > 0 { + // When sanitizing JSON strings make sure that we have 6 bytes if available. + if t.JSON && len(src) < 6 && !atEOF { + err = transform.ErrShortSrc + return + } + r, size := utf8.DecodeRune(src) + if r == utf8.RuneError && size < 2 { + if !atEOF { + err = transform.ErrShortSrc + return + } else { + err = errors.New("invalid UTF-8 string") + return + } + } + // Replace C0 and C1 control characters. + if unicode.IsControl(r) { + if repl, found := mapControlToCaret(r); found { + err = transfer(repl, src[:size]) + if err != nil { + return + } + continue + } + } + // Replace JSON C0 and C1 control characters. + if t.JSON && len(src) >= 6 { + if repl, found := mapJSONControlToCaret(src[:6]); found { + if t.addEscape { + // Add an escape character when necessary to prevent creating + // invalid JSON with our replacements. + repl = append([]byte{'\\'}, repl...) + t.addEscape = false + } + err = transfer(repl, src[:6]) + if err != nil { + return + } + continue + } + } + err = transfer(src[:size], src[:size]) + if err != nil { + return + } + if t.JSON { + if r == '\\' { + t.addEscape = !t.addEscape + } else { + t.addEscape = false + } + } + } + return +} + +// Reset resets the state and allows the Sanitizer to be reused. +func (t *Sanitizer) Reset() { + t.addEscape = false +} + +// mapControlToCaret maps C0 and C1 control characters to their caret notation. +func mapControlToCaret(r rune) ([]byte, bool) { + //\t (09), \n (10), \v (11), \r (13) are safe C0 characters and are not sanitized. + m := map[rune]string{ + 0: `^@`, + 1: `^A`, + 2: `^B`, + 3: `^C`, + 4: `^D`, + 5: `^E`, + 6: `^F`, + 7: `^G`, + 8: `^H`, + 12: `^L`, + 14: `^N`, + 15: `^O`, + 16: `^P`, + 17: `^Q`, + 18: `^R`, + 19: `^S`, + 20: `^T`, + 21: `^U`, + 22: `^V`, + 23: `^W`, + 24: `^X`, + 25: `^Y`, + 26: `^Z`, + 27: `^[`, + 28: `^\\`, + 29: `^]`, + 30: `^^`, + 31: `^_`, + 128: `^@`, + 129: `^A`, + 130: `^B`, + 131: `^C`, + 132: `^D`, + 133: `^E`, + 134: `^F`, + 135: `^G`, + 136: `^H`, + 137: `^I`, + 138: `^J`, + 139: `^K`, + 140: `^L`, + 141: `^M`, + 142: `^N`, + 143: `^O`, + 144: `^P`, + 145: `^Q`, + 146: `^R`, + 147: `^S`, + 148: `^T`, + 149: `^U`, + 150: `^V`, + 151: `^W`, + 152: `^X`, + 153: `^Y`, + 154: `^Z`, + 155: `^[`, + 156: `^\\`, + 157: `^]`, + 158: `^^`, + 159: `^_`, + } + if c, ok := m[r]; ok { + return []byte(c), true + } + return nil, false +} + +// mapJSONControlToCaret maps JSON C0 and C1 control characters to their caret notation. +// JSON control characters are six byte strings, representing a unicode code point, +// ranging from \u0000 to \u001F and \u0080 to \u009F. +func mapJSONControlToCaret(b []byte) ([]byte, bool) { + if len(b) != 6 { + return nil, false + } + if !bytes.HasPrefix(b, []byte(`\u00`)) { + return nil, false + } + //\t (\u0009), \n (\u000a), \v (\u000b), \r (\u000d) are safe C0 characters and are not sanitized. + m := map[string]string{ + `\u0000`: `^@`, + `\u0001`: `^A`, + `\u0002`: `^B`, + `\u0003`: `^C`, + `\u0004`: `^D`, + `\u0005`: `^E`, + `\u0006`: `^F`, + `\u0007`: `^G`, + `\u0008`: `^H`, + `\u000c`: `^L`, + `\u000e`: `^N`, + `\u000f`: `^O`, + `\u0010`: `^P`, + `\u0011`: `^Q`, + `\u0012`: `^R`, + `\u0013`: `^S`, + `\u0014`: `^T`, + `\u0015`: `^U`, + `\u0016`: `^V`, + `\u0017`: `^W`, + `\u0018`: `^X`, + `\u0019`: `^Y`, + `\u001a`: `^Z`, + `\u001b`: `^[`, + `\u001c`: `^\\`, + `\u001d`: `^]`, + `\u001e`: `^^`, + `\u001f`: `^_`, + `\u0080`: `^@`, + `\u0081`: `^A`, + `\u0082`: `^B`, + `\u0083`: `^C`, + `\u0084`: `^D`, + `\u0085`: `^E`, + `\u0086`: `^F`, + `\u0087`: `^G`, + `\u0088`: `^H`, + `\u0089`: `^I`, + `\u008a`: `^J`, + `\u008b`: `^K`, + `\u008c`: `^L`, + `\u008d`: `^M`, + `\u008e`: `^N`, + `\u008f`: `^O`, + `\u0090`: `^P`, + `\u0091`: `^Q`, + `\u0092`: `^R`, + `\u0093`: `^S`, + `\u0094`: `^T`, + `\u0095`: `^U`, + `\u0096`: `^V`, + `\u0097`: `^W`, + `\u0098`: `^X`, + `\u0099`: `^Y`, + `\u009a`: `^Z`, + `\u009b`: `^[`, + `\u009c`: `^\\`, + `\u009d`: `^]`, + `\u009e`: `^^`, + `\u009f`: `^_`, + } + if c, ok := m[strings.ToLower(string(b))]; ok { + return []byte(c), true + } + return nil, false +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/auth/auth.go b/vendor/github.com/cli/go-gh/v2/pkg/auth/auth.go new file mode 100644 index 000000000..6ab996f81 --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/auth/auth.go @@ -0,0 +1,194 @@ +// Package auth is a set of functions for retrieving authentication tokens +// and authenticated hosts. +package auth + +import ( + "fmt" + "os" + "os/exec" + "strconv" + "strings" + + "github.com/cli/go-gh/v2/internal/set" + "github.com/cli/go-gh/v2/pkg/config" + "github.com/cli/safeexec" +) + +const ( + codespaces = "CODESPACES" + defaultSource = "default" + ghEnterpriseToken = "GH_ENTERPRISE_TOKEN" + ghHost = "GH_HOST" + ghToken = "GH_TOKEN" + github = "github.com" + githubEnterpriseToken = "GITHUB_ENTERPRISE_TOKEN" + githubToken = "GITHUB_TOKEN" + hostsKey = "hosts" + localhost = "github.localhost" + oauthToken = "oauth_token" +) + +// TokenForHost retrieves an authentication token and the source of that token for the specified +// host. The source can be either an environment variable, configuration file, or the system +// keyring. In the latter case, this shells out to "gh auth token" to obtain the token. +// +// Returns "", "default" if no applicable token is found. +func TokenForHost(host string) (string, string) { + if token, source := TokenFromEnvOrConfig(host); token != "" { + return token, source + } + + ghExe := os.Getenv("GH_PATH") + if ghExe == "" { + ghExe, _ = safeexec.LookPath("gh") + } + + if ghExe != "" { + if token, source := tokenFromGh(ghExe, host); token != "" { + return token, source + } + } + + return "", defaultSource +} + +// TokenFromEnvOrConfig retrieves an authentication token from environment variables or the config +// file as fallback, but does not support reading the token from system keyring. Most consumers +// should use TokenForHost. +func TokenFromEnvOrConfig(host string) (string, string) { + cfg, _ := config.Read(nil) + return tokenForHost(cfg, host) +} + +func tokenForHost(cfg *config.Config, host string) (string, string) { + host = normalizeHostname(host) + if IsEnterprise(host) { + if token := os.Getenv(ghEnterpriseToken); token != "" { + return token, ghEnterpriseToken + } + if token := os.Getenv(githubEnterpriseToken); token != "" { + return token, githubEnterpriseToken + } + if isCodespaces, _ := strconv.ParseBool(os.Getenv(codespaces)); isCodespaces { + if token := os.Getenv(githubToken); token != "" { + return token, githubToken + } + } + if cfg != nil { + token, _ := cfg.Get([]string{hostsKey, host, oauthToken}) + return token, oauthToken + } + } + if token := os.Getenv(ghToken); token != "" { + return token, ghToken + } + if token := os.Getenv(githubToken); token != "" { + return token, githubToken + } + if cfg != nil { + token, _ := cfg.Get([]string{hostsKey, host, oauthToken}) + return token, oauthToken + } + return "", defaultSource +} + +func tokenFromGh(path string, host string) (string, string) { + cmd := exec.Command(path, "auth", "token", "--secure-storage", "--hostname", host) + result, err := cmd.Output() + if err != nil { + return "", "gh" + } + return strings.TrimSpace(string(result)), "gh" +} + +// KnownHosts retrieves a list of hosts that have corresponding +// authentication tokens, either from environment variables +// or from the configuration file. +// Returns an empty string slice if no hosts are found. +func KnownHosts() []string { + cfg, _ := config.Read(nil) + return knownHosts(cfg) +} + +func knownHosts(cfg *config.Config) []string { + hosts := set.NewStringSet() + if host := os.Getenv(ghHost); host != "" { + hosts.Add(host) + } + if token, _ := tokenForHost(cfg, github); token != "" { + hosts.Add(github) + } + if cfg != nil { + keys, err := cfg.Keys([]string{hostsKey}) + if err == nil { + hosts.AddValues(keys) + } + } + return hosts.ToSlice() +} + +// DefaultHost retrieves an authenticated host and the source of host. +// The source can be either an environment variable or from the +// configuration file. +// Returns "github.com", "default" if no viable host is found. +func DefaultHost() (string, string) { + cfg, _ := config.Read(nil) + return defaultHost(cfg) +} + +func defaultHost(cfg *config.Config) (string, string) { + if host := os.Getenv(ghHost); host != "" { + return host, ghHost + } + if cfg != nil { + keys, err := cfg.Keys([]string{hostsKey}) + if err == nil && len(keys) == 1 { + return keys[0], hostsKey + } + } + return github, defaultSource +} + +// TenancyHost is the domain name of a tenancy GitHub instance. +const tenancyHost = "ghe.com" + +// IsEnterprise determines if a provided host is a GitHub Enterprise Server instance, +// rather than GitHub.com or a tenancy GitHub instance. +func IsEnterprise(host string) bool { + normalizedHost := normalizeHostname(host) + return normalizedHost != github && normalizedHost != localhost && !IsTenancy(normalizedHost) +} + +// IsTenancy determines if a provided host is a tenancy GitHub instance, +// rather than GitHub.com or a GitHub Enterprise Server instance. +func IsTenancy(host string) bool { + normalizedHost := normalizeHostname(host) + return strings.HasSuffix(normalizedHost, "."+tenancyHost) +} + +func normalizeHostname(host string) string { + hostname := strings.ToLower(host) + if strings.HasSuffix(hostname, "."+github) { + return github + } + if strings.HasSuffix(hostname, "."+localhost) { + return localhost + } + // This has been copied over from the cli/cli NormalizeHostname function + // to ensure compatible behaviour but we don't fully understand when or + // why it would be useful here. We can't see what harm will come of + // duplicating the logic. + if before, found := cutSuffix(hostname, "."+tenancyHost); found { + idx := strings.LastIndex(before, ".") + return fmt.Sprintf("%s.%s", before[idx+1:], tenancyHost) + } + return hostname +} + +// Backport strings.CutSuffix from Go 1.20. +func cutSuffix(s, suffix string) (string, bool) { + if !strings.HasSuffix(s, suffix) { + return s, false + } + return s[:len(s)-len(suffix)], true +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/browser/browser.go b/vendor/github.com/cli/go-gh/v2/pkg/browser/browser.go new file mode 100644 index 000000000..4d567106d --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/browser/browser.go @@ -0,0 +1,80 @@ +// Package browser facilitates opening of URLs in a web browser. +package browser + +import ( + "io" + "os" + "os/exec" + + cliBrowser "github.com/cli/browser" + "github.com/cli/go-gh/v2/pkg/config" + "github.com/cli/safeexec" + "github.com/google/shlex" +) + +// Browser represents a web browser that can be used to open up URLs. +type Browser struct { + launcher string + stderr io.Writer + stdout io.Writer +} + +// New initializes a Browser. If a launcher is not specified +// one is determined based on environment variables or from the +// configuration file. +// The order of precedence for determining a launcher is: +// - Specified launcher; +// - GH_BROWSER environment variable; +// - browser option from configuration file; +// - BROWSER environment variable. +func New(launcher string, stdout, stderr io.Writer) *Browser { + if launcher == "" { + launcher = resolveLauncher() + } + b := &Browser{ + launcher: launcher, + stderr: stderr, + stdout: stdout, + } + return b +} + +// Browse opens the launcher and navigates to the specified URL. +func (b *Browser) Browse(url string) error { + return b.browse(url, nil) +} + +func (b *Browser) browse(url string, env []string) error { + if b.launcher == "" { + return cliBrowser.OpenURL(url) + } + launcherArgs, err := shlex.Split(b.launcher) + if err != nil { + return err + } + launcherExe, err := safeexec.LookPath(launcherArgs[0]) + if err != nil { + return err + } + args := append(launcherArgs[1:], url) + cmd := exec.Command(launcherExe, args...) + cmd.Stdout = b.stdout + cmd.Stderr = b.stderr + if env != nil { + cmd.Env = env + } + return cmd.Run() +} + +func resolveLauncher() string { + if ghBrowser := os.Getenv("GH_BROWSER"); ghBrowser != "" { + return ghBrowser + } + cfg, err := config.Read(nil) + if err == nil { + if cfgBrowser, _ := cfg.Get([]string{"browser"}); cfgBrowser != "" { + return cfgBrowser + } + } + return os.Getenv("BROWSER") +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/config/config.go b/vendor/github.com/cli/go-gh/v2/pkg/config/config.go new file mode 100644 index 000000000..0de46fe79 --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/config/config.go @@ -0,0 +1,336 @@ +// Package config is a set of types for interacting with the gh configuration files. +// Note: This package is intended for use only in gh, any other use cases are subject +// to breakage and non-backwards compatible updates. +package config + +import ( + "errors" + "io" + "os" + "path/filepath" + "runtime" + "sync" + + "github.com/cli/go-gh/v2/internal/yamlmap" +) + +const ( + appData = "AppData" + ghConfigDir = "GH_CONFIG_DIR" + localAppData = "LocalAppData" + xdgConfigHome = "XDG_CONFIG_HOME" + xdgDataHome = "XDG_DATA_HOME" + xdgStateHome = "XDG_STATE_HOME" + xdgCacheHome = "XDG_CACHE_HOME" +) + +var ( + cfg *Config + once sync.Once + loadErr error +) + +// Config is a in memory representation of the gh configuration files. +// It can be thought of as map where entries consist of a key that +// correspond to either a string value or a map value, allowing for +// multi-level maps. +type Config struct { + entries *yamlmap.Map + mu sync.RWMutex +} + +// Get a string value from a Config. +// The keys argument is a sequence of key values so that nested +// entries can be retrieved. A undefined string will be returned +// if trying to retrieve a key that corresponds to a map value. +// Returns "", KeyNotFoundError if any of the keys can not be found. +func (c *Config) Get(keys []string) (string, error) { + c.mu.RLock() + defer c.mu.RUnlock() + m := c.entries + for _, key := range keys { + var err error + m, err = m.FindEntry(key) + if err != nil { + return "", &KeyNotFoundError{key} + } + } + return m.Value, nil +} + +// Keys enumerates a Config's keys. +// The keys argument is a sequence of key values so that nested +// map values can be have their keys enumerated. +// Returns nil, KeyNotFoundError if any of the keys can not be found. +func (c *Config) Keys(keys []string) ([]string, error) { + c.mu.RLock() + defer c.mu.RUnlock() + m := c.entries + for _, key := range keys { + var err error + m, err = m.FindEntry(key) + if err != nil { + return nil, &KeyNotFoundError{key} + } + } + return m.Keys(), nil +} + +// Remove an entry from a Config. +// The keys argument is a sequence of key values so that nested +// entries can be removed. Removing an entry that has nested +// entries removes those also. +// Returns KeyNotFoundError if any of the keys can not be found. +func (c *Config) Remove(keys []string) error { + c.mu.Lock() + defer c.mu.Unlock() + m := c.entries + for i := 0; i < len(keys)-1; i++ { + var err error + key := keys[i] + m, err = m.FindEntry(key) + if err != nil { + return &KeyNotFoundError{key} + } + } + err := m.RemoveEntry(keys[len(keys)-1]) + if err != nil { + return &KeyNotFoundError{keys[len(keys)-1]} + } + return nil +} + +// Set a string value in a Config. +// The keys argument is a sequence of key values so that nested +// entries can be set. If any of the keys do not exist they will +// be created. If the string value to be set is empty it will be +// represented as null not an empty string when written. +// +// var c *Config +// c.Set([]string{"key"}, "") +// Write(c) // writes `key: ` not `key: ""` +func (c *Config) Set(keys []string, value string) { + c.mu.Lock() + defer c.mu.Unlock() + m := c.entries + for i := 0; i < len(keys)-1; i++ { + key := keys[i] + entry, err := m.FindEntry(key) + if err != nil { + entry = yamlmap.MapValue() + m.AddEntry(key, entry) + } + m = entry + } + val := yamlmap.StringValue(value) + if value == "" { + val = yamlmap.NullValue() + } + m.SetEntry(keys[len(keys)-1], val) +} + +func (c *Config) deepCopy() *Config { + return ReadFromString(c.entries.String()) +} + +// Read gh configuration files from the local file system and +// returns a Config. A copy of the fallback configuration will +// be returned when there are no configuration files to load. +// If there are no configuration files and no fallback configuration +// an empty configuration will be returned. +var Read = func(fallback *Config) (*Config, error) { + once.Do(func() { + cfg, loadErr = load(generalConfigFile(), hostsConfigFile(), fallback) + }) + return cfg, loadErr +} + +// ReadFromString takes a yaml string and returns a Config. +func ReadFromString(str string) *Config { + m, _ := mapFromString(str) + if m == nil { + m = yamlmap.MapValue() + } + return &Config{entries: m} +} + +// Write gh configuration files to the local file system. +// It will only write gh configuration files that have been modified +// since last being read. +func Write(c *Config) error { + c.mu.Lock() + defer c.mu.Unlock() + hosts, err := c.entries.FindEntry("hosts") + if err == nil && hosts.IsModified() { + err := writeFile(hostsConfigFile(), []byte(hosts.String())) + if err != nil { + return err + } + hosts.SetUnmodified() + } + + if c.entries.IsModified() { + // Hosts gets written to a different file above so remove it + // before writing and add it back in after writing. + hostsMap, hostsErr := c.entries.FindEntry("hosts") + if hostsErr == nil { + _ = c.entries.RemoveEntry("hosts") + } + err := writeFile(generalConfigFile(), []byte(c.entries.String())) + if err != nil { + return err + } + c.entries.SetUnmodified() + if hostsErr == nil { + c.entries.AddEntry("hosts", hostsMap) + } + } + + return nil +} + +func load(generalFilePath, hostsFilePath string, fallback *Config) (*Config, error) { + generalMap, err := mapFromFile(generalFilePath) + if err != nil && !os.IsNotExist(err) { + if errors.Is(err, yamlmap.ErrInvalidYaml) || + errors.Is(err, yamlmap.ErrInvalidFormat) { + return nil, &InvalidConfigFileError{Path: generalFilePath, Err: err} + } + return nil, err + } + + if generalMap == nil { + generalMap = yamlmap.MapValue() + } + + hostsMap, err := mapFromFile(hostsFilePath) + if err != nil && !os.IsNotExist(err) { + if errors.Is(err, yamlmap.ErrInvalidYaml) || + errors.Is(err, yamlmap.ErrInvalidFormat) { + return nil, &InvalidConfigFileError{Path: hostsFilePath, Err: err} + } + return nil, err + } + + if hostsMap != nil && !hostsMap.Empty() { + generalMap.AddEntry("hosts", hostsMap) + generalMap.SetUnmodified() + } + + if generalMap.Empty() && fallback != nil { + return fallback.deepCopy(), nil + } + + return &Config{entries: generalMap}, nil +} + +func generalConfigFile() string { + return filepath.Join(ConfigDir(), "config.yml") +} + +func hostsConfigFile() string { + return filepath.Join(ConfigDir(), "hosts.yml") +} + +func mapFromFile(filename string) (*yamlmap.Map, error) { + data, err := readFile(filename) + if err != nil { + return nil, err + } + return yamlmap.Unmarshal(data) +} + +func mapFromString(str string) (*yamlmap.Map, error) { + return yamlmap.Unmarshal([]byte(str)) +} + +// Config path precedence: GH_CONFIG_DIR, XDG_CONFIG_HOME, AppData (windows only), HOME. +func ConfigDir() string { + var path string + if a := os.Getenv(ghConfigDir); a != "" { + path = a + } else if b := os.Getenv(xdgConfigHome); b != "" { + path = filepath.Join(b, "gh") + } else if c := os.Getenv(appData); runtime.GOOS == "windows" && c != "" { + path = filepath.Join(c, "GitHub CLI") + } else { + d, _ := os.UserHomeDir() + path = filepath.Join(d, ".config", "gh") + } + return path +} + +// State path precedence: XDG_STATE_HOME, LocalAppData (windows only), HOME. +func StateDir() string { + var path string + if a := os.Getenv(xdgStateHome); a != "" { + path = filepath.Join(a, "gh") + } else if b := os.Getenv(localAppData); runtime.GOOS == "windows" && b != "" { + path = filepath.Join(b, "GitHub CLI") + } else { + c, _ := os.UserHomeDir() + path = filepath.Join(c, ".local", "state", "gh") + } + return path +} + +// Data path precedence: XDG_DATA_HOME, LocalAppData (windows only), HOME. +func DataDir() string { + var path string + if a := os.Getenv(xdgDataHome); a != "" { + path = filepath.Join(a, "gh") + } else if b := os.Getenv(localAppData); runtime.GOOS == "windows" && b != "" { + path = filepath.Join(b, "GitHub CLI") + } else { + c, _ := os.UserHomeDir() + path = filepath.Join(c, ".local", "share", "gh") + } + return path +} + +// Cache path precedence: XDG_CACHE_HOME, LocalAppData (windows only), HOME, legacy gh-cli-cache. +func CacheDir() string { + if a := os.Getenv(xdgCacheHome); a != "" { + return filepath.Join(a, "gh") + } else if b := os.Getenv(localAppData); runtime.GOOS == "windows" && b != "" { + return filepath.Join(b, "GitHub CLI") + } else if c, err := os.UserHomeDir(); err == nil { + return filepath.Join(c, ".cache", "gh") + } else { + // Note that this has a minor security issue because /tmp is world-writeable. + // As such, it is possible for other users on a shared system to overwrite cached data. + // The practical risk of this is low, but it's worth calling out as a risk. + // I've included this here for backwards compatibility but we should consider removing it. + return filepath.Join(os.TempDir(), "gh-cli-cache") + } +} + +func readFile(filename string) ([]byte, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + data, err := io.ReadAll(f) + if err != nil { + return nil, err + } + return data, nil +} + +func writeFile(filename string, data []byte) (writeErr error) { + if writeErr = os.MkdirAll(filepath.Dir(filename), 0771); writeErr != nil { + return + } + var file *os.File + if file, writeErr = os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600); writeErr != nil { + return + } + defer func() { + if err := file.Close(); writeErr == nil && err != nil { + writeErr = err + } + }() + _, writeErr = file.Write(data) + return +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/config/errors.go b/vendor/github.com/cli/go-gh/v2/pkg/config/errors.go new file mode 100644 index 000000000..1aefd1922 --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/config/errors.go @@ -0,0 +1,32 @@ +package config + +import ( + "fmt" +) + +// InvalidConfigFileError represents an error when trying to read a config file. +type InvalidConfigFileError struct { + Path string + Err error +} + +// Allow InvalidConfigFileError to satisfy error interface. +func (e *InvalidConfigFileError) Error() string { + return fmt.Sprintf("invalid config file %s: %s", e.Path, e.Err) +} + +// Allow InvalidConfigFileError to be unwrapped. +func (e *InvalidConfigFileError) Unwrap() error { + return e.Err +} + +// KeyNotFoundError represents an error when trying to find a config key +// that does not exist. +type KeyNotFoundError struct { + Key string +} + +// Allow KeyNotFoundError to satisfy error interface. +func (e *KeyNotFoundError) Error() string { + return fmt.Sprintf("could not find key %q", e.Key) +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/jq/jq.go b/vendor/github.com/cli/go-gh/v2/pkg/jq/jq.go new file mode 100644 index 000000000..ade2308be --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/jq/jq.go @@ -0,0 +1,159 @@ +// Package jq facilitates processing of JSON strings using jq expressions. +package jq + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "os" + "strconv" + "strings" + + "github.com/cli/go-gh/v2/pkg/jsonpretty" + "github.com/itchyny/gojq" +) + +// Evaluate a jq expression against an input and write it to an output. +// Any top-level scalar values produced by the jq expression are written out +// directly, as raw values and not as JSON scalars, similar to how jq --raw +// works. +func Evaluate(input io.Reader, output io.Writer, expr string) error { + return EvaluateFormatted(input, output, expr, "", false) +} + +// Evaluate a jq expression against an input and write it to an output, +// optionally with indentation and colorization. Any top-level scalar values +// produced by the jq expression are written out directly, as raw values and not +// as JSON scalars, similar to how jq --raw works. +func EvaluateFormatted(input io.Reader, output io.Writer, expr string, indent string, colorize bool) error { + query, err := gojq.Parse(expr) + if err != nil { + var e *gojq.ParseError + if errors.As(err, &e) { + str, line, column := getLineColumn(expr, e.Offset-len(e.Token)) + return fmt.Errorf( + "failed to parse jq expression (line %d, column %d)\n %s\n %*c %w", + line, column, str, column, '^', err, + ) + } + return err + } + + code, err := gojq.Compile( + query, + gojq.WithEnvironLoader(func() []string { + return os.Environ() + })) + if err != nil { + return err + } + + jsonData, err := io.ReadAll(input) + if err != nil { + return err + } + + var responseData interface{} + err = json.Unmarshal(jsonData, &responseData) + if err != nil { + return err + } + + enc := prettyEncoder{ + w: output, + indent: indent, + colorize: colorize, + } + + iter := code.Run(responseData) + for { + v, ok := iter.Next() + if !ok { + break + } + if err, isErr := v.(error); isErr { + var e *gojq.HaltError + if errors.As(err, &e) && e.Value() == nil { + break + } + return err + } + if text, e := jsonScalarToString(v); e == nil { + _, err := fmt.Fprintln(output, text) + if err != nil { + return err + } + } else { + if err = enc.Encode(v); err != nil { + return err + } + } + } + + return nil +} + +func jsonScalarToString(input interface{}) (string, error) { + switch tt := input.(type) { + case string: + return tt, nil + case float64: + if math.Trunc(tt) == tt { + return strconv.FormatFloat(tt, 'f', 0, 64), nil + } else { + return strconv.FormatFloat(tt, 'f', 2, 64), nil + } + case nil: + return "", nil + case bool: + return fmt.Sprintf("%v", tt), nil + default: + return "", fmt.Errorf("cannot convert type to string: %v", tt) + } +} + +type prettyEncoder struct { + w io.Writer + indent string + colorize bool +} + +func (p prettyEncoder) Encode(v any) error { + var b []byte + var err error + if p.indent == "" { + b, err = json.Marshal(v) + } else { + b, err = json.MarshalIndent(v, "", p.indent) + } + if err != nil { + return err + } + if !p.colorize { + if _, err := p.w.Write(b); err != nil { + return err + } + if _, err := p.w.Write([]byte{'\n'}); err != nil { + return err + } + return nil + } + return jsonpretty.Format(p.w, bytes.NewReader(b), p.indent, true) +} + +func getLineColumn(expr string, offset int) (string, int, int) { + for line := 1; ; line++ { + index := strings.Index(expr, "\n") + if index < 0 { + return expr, line, offset + 1 + } + if index >= offset { + return expr[:index], line, offset + 1 + } + expr = expr[index+1:] + offset -= index + 1 + } +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/jsonpretty/format.go b/vendor/github.com/cli/go-gh/v2/pkg/jsonpretty/format.go new file mode 100644 index 000000000..63ac972fc --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/jsonpretty/format.go @@ -0,0 +1,144 @@ +// Package jsonpretty implements a terminal pretty-printer for JSON. +package jsonpretty + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "strings" +) + +const ( + colorDelim = "\x1b[1;38m" // bright white + colorKey = "\x1b[1;34m" // bright blue + colorNull = "\x1b[36m" // cyan + colorString = "\x1b[32m" // green + colorBool = "\x1b[33m" // yellow + colorReset = "\x1b[m" +) + +// Format reads JSON from r and writes a prettified version of it to w. +func Format(w io.Writer, r io.Reader, indent string, colorize bool) error { + dec := json.NewDecoder(r) + dec.UseNumber() + + c := func(ansi string) string { + if !colorize { + return "" + } + return ansi + } + + var idx int + var stack []json.Delim + + for { + t, err := dec.Token() + if err == io.EOF { + break + } + if err != nil { + return err + } + + switch tt := t.(type) { + case json.Delim: + switch tt { + case '{', '[': + stack = append(stack, tt) + idx = 0 + if _, err := fmt.Fprint(w, c(colorDelim), tt, c(colorReset)); err != nil { + return err + } + if dec.More() { + if _, err := fmt.Fprint(w, "\n", strings.Repeat(indent, len(stack))); err != nil { + return err + } + } + continue + case '}', ']': + stack = stack[:len(stack)-1] + idx = 0 + if _, err := fmt.Fprint(w, c(colorDelim), tt, c(colorReset)); err != nil { + return err + } + } + default: + b, err := marshalJSON(tt) + if err != nil { + return err + } + + isKey := len(stack) > 0 && stack[len(stack)-1] == '{' && idx%2 == 0 + idx++ + + var color string + if isKey { + color = colorKey + } else if tt == nil { + color = colorNull + } else { + switch t.(type) { + case string: + color = colorString + case bool: + color = colorBool + } + } + + if color != "" { + if _, err := fmt.Fprint(w, c(color)); err != nil { + return err + } + } + if _, err := w.Write(b); err != nil { + return err + } + if color != "" { + if _, err := fmt.Fprint(w, c(colorReset)); err != nil { + return err + } + } + + if isKey { + if _, err := fmt.Fprint(w, c(colorDelim), ":", c(colorReset), " "); err != nil { + return err + } + continue + } + } + + if dec.More() { + if _, err := fmt.Fprint(w, c(colorDelim), ",", c(colorReset), "\n", strings.Repeat(indent, len(stack))); err != nil { + return err + } + } else if len(stack) > 0 { + if _, err := fmt.Fprint(w, "\n", strings.Repeat(indent, len(stack)-1)); err != nil { + return err + } + } else { + if _, err := fmt.Fprint(w, "\n"); err != nil { + return err + } + } + } + + return nil +} + +// marshalJSON works like json.Marshal, but with HTML-escaping disabled. +func marshalJSON(v interface{}) ([]byte, error) { + buf := bytes.Buffer{} + enc := json.NewEncoder(&buf) + enc.SetEscapeHTML(false) + if err := enc.Encode(v); err != nil { + return nil, err + } + bb := buf.Bytes() + // omit trailing newline added by json.Encoder + if len(bb) > 0 && bb[len(bb)-1] == '\n' { + return bb[:len(bb)-1], nil + } + return bb, nil +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/prompter/mock.go b/vendor/github.com/cli/go-gh/v2/pkg/prompter/mock.go new file mode 100644 index 000000000..c5694239c --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/prompter/mock.go @@ -0,0 +1,228 @@ +package prompter + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +// PrompterMock provides stubbed out methods for prompting the user for +// use in tests. PrompterMock has a superset of the methods on Prompter +// so they both can satisfy the same interface. +// +// A basic example of how PrompterMock can be used: +// +// type ConfirmPrompter interface { +// Confirm(string, bool) (bool, error) +// } +// +// func PlayGame(prompter ConfirmPrompter) (int, error) { +// confirm, err := prompter.Confirm("Shall we play a game", true) +// if err != nil { +// return 0, err +// } +// if confirm { +// return 1, nil +// } +// return 2, nil +// } +// +// func TestPlayGame(t *testing.T) { +// expectedOutcome := 1 +// mock := NewMock(t) +// mock.RegisterConfirm("Shall we play a game", func(prompt string, defaultValue bool) (bool, error) { +// return true, nil +// }) +// outcome, err := PlayGame(mock) +// if err != nil { +// t.Fatalf("unexpected error: %v", err) +// } +// if outcome != expectedOutcome { +// t.Errorf("expected %q, got %q", expectedOutcome, outcome) +// } +// } +type PrompterMock struct { + t *testing.T + selectStubs []selectStub + multiSelectStubs []multiSelectStub + inputStubs []inputStub + passwordStubs []passwordStub + confirmStubs []confirmStub +} + +type selectStub struct { + prompt string + expectedOptions []string + fn func(string, string, []string) (int, error) +} + +type multiSelectStub struct { + prompt string + expectedOptions []string + fn func(string, []string, []string) ([]int, error) +} + +type inputStub struct { + prompt string + fn func(string, string) (string, error) +} + +type passwordStub struct { + prompt string + fn func(string) (string, error) +} + +type confirmStub struct { + Prompt string + Fn func(string, bool) (bool, error) +} + +// NewMock instantiates a new PrompterMock. +func NewMock(t *testing.T) *PrompterMock { + m := &PrompterMock{ + t: t, + selectStubs: []selectStub{}, + multiSelectStubs: []multiSelectStub{}, + inputStubs: []inputStub{}, + passwordStubs: []passwordStub{}, + confirmStubs: []confirmStub{}, + } + t.Cleanup(m.verify) + return m +} + +// Select prompts the user to select an option from a list of options. +func (m *PrompterMock) Select(prompt, defaultValue string, options []string) (int, error) { + var s selectStub + if len(m.selectStubs) == 0 { + return -1, noSuchPromptErr(prompt) + } + s = m.selectStubs[0] + m.selectStubs = m.selectStubs[1:len(m.selectStubs)] + if s.prompt != prompt { + return -1, noSuchPromptErr(prompt) + } + assertOptions(m.t, s.expectedOptions, options) + return s.fn(prompt, defaultValue, options) +} + +// MultiSelect prompts the user to select multiple options from a list of options. +func (m *PrompterMock) MultiSelect(prompt string, defaultValues, options []string) ([]int, error) { + var s multiSelectStub + if len(m.multiSelectStubs) == 0 { + return []int{}, noSuchPromptErr(prompt) + } + s = m.multiSelectStubs[0] + m.multiSelectStubs = m.multiSelectStubs[1:len(m.multiSelectStubs)] + if s.prompt != prompt { + return []int{}, noSuchPromptErr(prompt) + } + assertOptions(m.t, s.expectedOptions, options) + return s.fn(prompt, defaultValues, options) +} + +// Input prompts the user to input a single-line string. +func (m *PrompterMock) Input(prompt, defaultValue string) (string, error) { + var s inputStub + if len(m.inputStubs) == 0 { + return "", noSuchPromptErr(prompt) + } + s = m.inputStubs[0] + m.inputStubs = m.inputStubs[1:len(m.inputStubs)] + if s.prompt != prompt { + return "", noSuchPromptErr(prompt) + } + return s.fn(prompt, defaultValue) +} + +// Password prompts the user to input a single-line string without echoing the input. +func (m *PrompterMock) Password(prompt string) (string, error) { + var s passwordStub + if len(m.passwordStubs) == 0 { + return "", noSuchPromptErr(prompt) + } + s = m.passwordStubs[0] + m.passwordStubs = m.passwordStubs[1:len(m.passwordStubs)] + if s.prompt != prompt { + return "", noSuchPromptErr(prompt) + } + return s.fn(prompt) +} + +// Confirm prompts the user to confirm a yes/no question. +func (m *PrompterMock) Confirm(prompt string, defaultValue bool) (bool, error) { + var s confirmStub + if len(m.confirmStubs) == 0 { + return false, noSuchPromptErr(prompt) + } + s = m.confirmStubs[0] + m.confirmStubs = m.confirmStubs[1:len(m.confirmStubs)] + if s.Prompt != prompt { + return false, noSuchPromptErr(prompt) + } + return s.Fn(prompt, defaultValue) +} + +// RegisterSelect records that a Select prompt should be called. +func (m *PrompterMock) RegisterSelect(prompt string, opts []string, stub func(_, _ string, _ []string) (int, error)) { + m.selectStubs = append(m.selectStubs, selectStub{ + prompt: prompt, + expectedOptions: opts, + fn: stub}) +} + +// RegisterMultiSelect records that a MultiSelect prompt should be called. +func (m *PrompterMock) RegisterMultiSelect(prompt string, d, opts []string, stub func(_ string, _, _ []string) ([]int, error)) { + m.multiSelectStubs = append(m.multiSelectStubs, multiSelectStub{ + prompt: prompt, + expectedOptions: opts, + fn: stub}) +} + +// RegisterInput records that an Input prompt should be called. +func (m *PrompterMock) RegisterInput(prompt string, stub func(_, _ string) (string, error)) { + m.inputStubs = append(m.inputStubs, inputStub{prompt: prompt, fn: stub}) +} + +// RegisterPassword records that a Password prompt should be called. +func (m *PrompterMock) RegisterPassword(prompt string, stub func(string) (string, error)) { + m.passwordStubs = append(m.passwordStubs, passwordStub{prompt: prompt, fn: stub}) +} + +// RegisterConfirm records that a Confirm prompt should be called. +func (m *PrompterMock) RegisterConfirm(prompt string, stub func(_ string, _ bool) (bool, error)) { + m.confirmStubs = append(m.confirmStubs, confirmStub{Prompt: prompt, Fn: stub}) +} + +func (m *PrompterMock) verify() { + errs := []string{} + if len(m.selectStubs) > 0 { + errs = append(errs, "MultiSelect") + } + if len(m.multiSelectStubs) > 0 { + errs = append(errs, "Select") + } + if len(m.inputStubs) > 0 { + errs = append(errs, "Input") + } + if len(m.passwordStubs) > 0 { + errs = append(errs, "Password") + } + if len(m.confirmStubs) > 0 { + errs = append(errs, "Confirm") + } + if len(errs) > 0 { + m.t.Helper() + m.t.Errorf("%d unmatched calls to %s", len(errs), strings.Join(errs, ",")) + } +} + +func noSuchPromptErr(prompt string) error { + return fmt.Errorf("no such prompt '%s'", prompt) +} + +func assertOptions(t *testing.T, expected, actual []string) { + assert.Equal(t, expected, actual) +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/prompter/prompter.go b/vendor/github.com/cli/go-gh/v2/pkg/prompter/prompter.go new file mode 100644 index 000000000..1cc184916 --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/prompter/prompter.go @@ -0,0 +1,134 @@ +// Package prompter provides various methods for prompting the user with +// questions for input. +package prompter + +import ( + "fmt" + "io" + "strings" + + "github.com/AlecAivazis/survey/v2" + "github.com/cli/go-gh/v2/pkg/text" +) + +// Prompter provides methods for prompting the user. +type Prompter struct { + stdin FileReader + stdout FileWriter + stderr FileWriter +} + +// FileWriter provides a minimal writable interface for stdout and stderr. +type FileWriter interface { + io.Writer + Fd() uintptr +} + +// FileReader provides a minimal readable interface for stdin. +type FileReader interface { + io.Reader + Fd() uintptr +} + +// New instantiates a new Prompter. +func New(stdin FileReader, stdout FileWriter, stderr FileWriter) *Prompter { + return &Prompter{ + stdin: stdin, + stdout: stdout, + stderr: stderr, + } +} + +// Select prompts the user to select an option from a list of options. +func (p *Prompter) Select(prompt, defaultValue string, options []string) (int, error) { + var result int + q := &survey.Select{ + Message: prompt, + Options: options, + PageSize: 20, + Filter: latinMatchingFilter, + } + if defaultValue != "" { + for _, o := range options { + if o == defaultValue { + q.Default = defaultValue + break + } + } + } + err := p.ask(q, &result) + return result, err +} + +// MultiSelect prompts the user to select multiple options from a list of options. +func (p *Prompter) MultiSelect(prompt string, defaultValues, options []string) ([]int, error) { + var result []int + q := &survey.MultiSelect{ + Message: prompt, + Options: options, + PageSize: 20, + Filter: latinMatchingFilter, + } + if len(defaultValues) > 0 { + validatedDefault := []string{} + for _, x := range defaultValues { + for _, y := range options { + if x == y { + validatedDefault = append(validatedDefault, x) + } + } + } + q.Default = validatedDefault + } + err := p.ask(q, &result) + return result, err +} + +// Input prompts the user to input a single-line string. +func (p *Prompter) Input(prompt, defaultValue string) (string, error) { + var result string + err := p.ask(&survey.Input{ + Message: prompt, + Default: defaultValue, + }, &result) + return result, err +} + +// Password prompts the user to input a single-line string without echoing the input. +func (p *Prompter) Password(prompt string) (string, error) { + var result string + err := p.ask(&survey.Password{ + Message: prompt, + }, &result) + return result, err +} + +// Confirm prompts the user to confirm a yes/no question. +func (p *Prompter) Confirm(prompt string, defaultValue bool) (bool, error) { + var result bool + err := p.ask(&survey.Confirm{ + Message: prompt, + Default: defaultValue, + }, &result) + return result, err +} + +func (p *Prompter) ask(q survey.Prompt, response interface{}, opts ...survey.AskOpt) error { + opts = append(opts, survey.WithStdio(p.stdin, p.stdout, p.stderr)) + err := survey.AskOne(q, response, opts...) + if err == nil { + return nil + } + return fmt.Errorf("could not prompt: %w", err) +} + +// latinMatchingFilter returns whether the value matches the input filter. +// The strings are compared normalized in case. +// The filter's diactritics are kept as-is, but the value's are normalized, +// so that a missing diactritic in the filter still returns a result. +func latinMatchingFilter(filter, value string, index int) bool { + filter = strings.ToLower(filter) + value = strings.ToLower(value) + // include this option if it matches. + return strings.Contains(value, filter) || strings.Contains(text.RemoveDiacritics(value), filter) +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/repository/repository.go b/vendor/github.com/cli/go-gh/v2/pkg/repository/repository.go new file mode 100644 index 000000000..2d3400b53 --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/repository/repository.go @@ -0,0 +1,158 @@ +// Package repository is a set of types and functions for modeling and +// interacting with GitHub repositories. +package repository + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/cli/go-gh/v2/internal/git" + "github.com/cli/go-gh/v2/pkg/auth" + "github.com/cli/go-gh/v2/pkg/ssh" +) + +// Repository holds information representing a GitHub repository. +type Repository struct { + Host string + Name string + Owner string +} + +// Parse extracts the repository information from the following +// string formats: "OWNER/REPO", "HOST/OWNER/REPO", and a full URL. +// If the format does not specify a host, use the config to determine a host. +func Parse(s string) (Repository, error) { + var r Repository + + if git.IsURL(s) { + u, err := git.ParseURL(s) + if err != nil { + return r, err + } + + host, owner, name, err := git.RepoInfoFromURL(u) + if err != nil { + return r, err + } + + r.Host = host + r.Name = name + r.Owner = owner + + return r, nil + } + + parts := strings.SplitN(s, "/", 4) + for _, p := range parts { + if len(p) == 0 { + return r, fmt.Errorf(`expected the "[HOST/]OWNER/REPO" format, got %q`, s) + } + } + + switch len(parts) { + case 3: + r.Host = parts[0] + r.Owner = parts[1] + r.Name = parts[2] + return r, nil + case 2: + r.Host, _ = auth.DefaultHost() + r.Owner = parts[0] + r.Name = parts[1] + return r, nil + default: + return r, fmt.Errorf(`expected the "[HOST/]OWNER/REPO" format, got %q`, s) + } +} + +// Parse extracts the repository information from the following +// string formats: "OWNER/REPO", "HOST/OWNER/REPO", and a full URL. +// If the format does not specify a host, use the host provided. +func ParseWithHost(s, host string) (Repository, error) { + var r Repository + + if git.IsURL(s) { + u, err := git.ParseURL(s) + if err != nil { + return r, err + } + + host, owner, name, err := git.RepoInfoFromURL(u) + if err != nil { + return r, err + } + + r.Host = host + r.Owner = owner + r.Name = name + + return r, nil + } + + parts := strings.SplitN(s, "/", 4) + for _, p := range parts { + if len(p) == 0 { + return r, fmt.Errorf(`expected the "[HOST/]OWNER/REPO" format, got %q`, s) + } + } + + switch len(parts) { + case 3: + r.Host = parts[0] + r.Owner = parts[1] + r.Name = parts[2] + return r, nil + case 2: + r.Host = host + r.Owner = parts[0] + r.Name = parts[1] + return r, nil + default: + return r, fmt.Errorf(`expected the "[HOST/]OWNER/REPO" format, got %q`, s) + } +} + +// Current uses git remotes to determine the GitHub repository +// the current directory is tracking. +func Current() (Repository, error) { + var r Repository + + override := os.Getenv("GH_REPO") + if override != "" { + return Parse(override) + } + + remotes, err := git.Remotes() + if err != nil { + return r, err + } + if len(remotes) == 0 { + return r, errors.New("unable to determine current repository, no git remotes configured for this repository") + } + + translator := ssh.NewTranslator() + for _, r := range remotes { + if r.FetchURL != nil { + r.FetchURL = translator.Translate(r.FetchURL) + } + if r.PushURL != nil { + r.PushURL = translator.Translate(r.PushURL) + } + } + + hosts := auth.KnownHosts() + + filteredRemotes := remotes.FilterByHosts(hosts) + if len(filteredRemotes) == 0 { + return r, errors.New("unable to determine current repository, none of the git remotes configured for this repository point to a known GitHub host") + } + + rem := filteredRemotes[0] + r.Host = rem.Host + r.Owner = rem.Owner + r.Name = rem.Repo + + return r, nil +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/ssh/ssh.go b/vendor/github.com/cli/go-gh/v2/pkg/ssh/ssh.go new file mode 100644 index 000000000..4e5216e7d --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/ssh/ssh.go @@ -0,0 +1,109 @@ +// Package ssh resolves local SSH hostname aliases. +package ssh + +import ( + "bufio" + "net/url" + "os/exec" + "strings" + "sync" + + "github.com/cli/safeexec" +) + +type Translator struct { + cacheMap map[string]string + cacheMu sync.RWMutex + sshPath string + sshPathErr error + sshPathMu sync.Mutex + + lookPath func(string) (string, error) + newCommand func(string, ...string) *exec.Cmd +} + +// NewTranslator initializes a new Translator instance. +func NewTranslator() *Translator { + return &Translator{} +} + +// Translate applies applicable SSH hostname aliases to the specified URL and returns the resulting URL. +func (t *Translator) Translate(u *url.URL) *url.URL { + if u.Scheme != "ssh" { + return u + } + resolvedHost, err := t.resolve(u.Hostname()) + if err != nil { + return u + } + if strings.EqualFold(resolvedHost, "ssh.github.com") { + resolvedHost = "github.com" + } + newURL, _ := url.Parse(u.String()) + newURL.Host = resolvedHost + return newURL +} + +func (t *Translator) resolve(hostname string) (string, error) { + t.cacheMu.RLock() + cached, cacheFound := t.cacheMap[strings.ToLower(hostname)] + t.cacheMu.RUnlock() + if cacheFound { + return cached, nil + } + + var sshPath string + t.sshPathMu.Lock() + if t.sshPath == "" && t.sshPathErr == nil { + lookPath := t.lookPath + if lookPath == nil { + lookPath = safeexec.LookPath + } + t.sshPath, t.sshPathErr = lookPath("ssh") + } + if t.sshPathErr != nil { + defer t.sshPathMu.Unlock() + return t.sshPath, t.sshPathErr + } + sshPath = t.sshPath + t.sshPathMu.Unlock() + + t.cacheMu.Lock() + defer t.cacheMu.Unlock() + + newCommand := t.newCommand + if newCommand == nil { + newCommand = exec.Command + } + sshCmd := newCommand(sshPath, "-G", hostname) + stdout, err := sshCmd.StdoutPipe() + if err != nil { + return "", err + } + + if err := sshCmd.Start(); err != nil { + return "", err + } + + var resolvedHost string + s := bufio.NewScanner(stdout) + for s.Scan() { + line := s.Text() + parts := strings.SplitN(line, " ", 2) + if len(parts) == 2 && parts[0] == "hostname" { + resolvedHost = parts[1] + } + } + + err = sshCmd.Wait() + if err != nil || resolvedHost == "" { + // handle failures by returning the original hostname unchanged + resolvedHost = hostname + } + + if t.cacheMap == nil { + t.cacheMap = map[string]string{} + } + t.cacheMap[strings.ToLower(hostname)] = resolvedHost + return resolvedHost, nil +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/tableprinter/table.go b/vendor/github.com/cli/go-gh/v2/pkg/tableprinter/table.go new file mode 100644 index 000000000..15217abdd --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/tableprinter/table.go @@ -0,0 +1,259 @@ +// Package tableprinter facilitates rendering column-formatted data to a terminal and TSV-formatted data to +// a script or a file. It is suitable for presenting tabular data in a human-readable format that is +// guaranteed to fit within the given viewport, while at the same time offering the same data in a +// machine-readable format for scripts. +package tableprinter + +import ( + "fmt" + "io" + + "github.com/cli/go-gh/v2/pkg/text" +) + +type fieldOption func(*tableField) + +type TablePrinter interface { + AddHeader([]string, ...fieldOption) + AddField(string, ...fieldOption) + EndRow() + Render() error +} + +// WithTruncate overrides the truncation function for the field. The function should transform a string +// argument into a string that fits within the given display width. The default behavior is to truncate the +// value by adding "..." in the end. The truncation function will be called before padding and coloring. +// Pass nil to disable truncation for this value. +func WithTruncate(fn func(int, string) string) fieldOption { + return func(f *tableField) { + f.truncateFunc = fn + } +} + +// WithPadding overrides the padding function for the field. The function should transform a string argument +// into a string that is padded to fit within the given display width. The default behavior is to pad fields +// with spaces except for the last field. The padding function will be called after truncation and before coloring. +// Pass nil to disable padding for this value. +func WithPadding(fn func(int, string) string) fieldOption { + return func(f *tableField) { + f.paddingFunc = fn + } +} + +// WithColor sets the color function for the field. The function should transform a string value by wrapping +// it in ANSI escape codes. The color function will not be used if the table was initialized in non-terminal mode. +// The color function will be called before truncation and padding. +func WithColor(fn func(string) string) fieldOption { + return func(f *tableField) { + f.colorFunc = fn + } +} + +// New initializes a table printer with terminal mode and terminal width. When terminal mode is enabled, the +// output will be human-readable, column-formatted to fit available width, and rendered with color support. +// In non-terminal mode, the output is tab-separated and all truncation of values is disabled. +func New(w io.Writer, isTTY bool, maxWidth int) TablePrinter { + if isTTY { + return &ttyTablePrinter{ + out: w, + maxWidth: maxWidth, + } + } + + return &tsvTablePrinter{ + out: w, + } +} + +type tableField struct { + text string + truncateFunc func(int, string) string + paddingFunc func(int, string) string + colorFunc func(string) string +} + +type ttyTablePrinter struct { + out io.Writer + maxWidth int + hasHeaders bool + rows [][]tableField +} + +func (t *ttyTablePrinter) AddHeader(columns []string, opts ...fieldOption) { + if t.hasHeaders { + return + } + + t.hasHeaders = true + for _, column := range columns { + t.AddField(column, opts...) + } + t.EndRow() +} + +func (t *ttyTablePrinter) AddField(s string, opts ...fieldOption) { + if t.rows == nil { + t.rows = make([][]tableField, 1) + } + rowI := len(t.rows) - 1 + field := tableField{ + text: s, + truncateFunc: text.Truncate, + } + for _, opt := range opts { + opt(&field) + } + t.rows[rowI] = append(t.rows[rowI], field) +} + +func (t *ttyTablePrinter) EndRow() { + t.rows = append(t.rows, []tableField{}) +} + +func (t *ttyTablePrinter) Render() error { + if len(t.rows) == 0 { + return nil + } + + delim := " " + numCols := len(t.rows[0]) + colWidths := t.calculateColumnWidths(len(delim)) + + for _, row := range t.rows { + for col, field := range row { + if col > 0 { + _, err := fmt.Fprint(t.out, delim) + if err != nil { + return err + } + } + truncVal := field.text + if field.truncateFunc != nil { + truncVal = field.truncateFunc(colWidths[col], field.text) + } + if field.paddingFunc != nil { + truncVal = field.paddingFunc(colWidths[col], truncVal) + } else if col < numCols-1 { + truncVal = text.PadRight(colWidths[col], truncVal) + } + if field.colorFunc != nil { + truncVal = field.colorFunc(truncVal) + } + _, err := fmt.Fprint(t.out, truncVal) + if err != nil { + return err + } + } + if len(row) > 0 { + _, err := fmt.Fprint(t.out, "\n") + if err != nil { + return err + } + } + } + return nil +} + +func (t *ttyTablePrinter) calculateColumnWidths(delimSize int) []int { + numCols := len(t.rows[0]) + maxColWidths := make([]int, numCols) + colWidths := make([]int, numCols) + + for _, row := range t.rows { + for col, field := range row { + w := text.DisplayWidth(field.text) + if w > maxColWidths[col] { + maxColWidths[col] = w + } + // if this field has disabled truncating, ensure that the column is wide enough + if field.truncateFunc == nil && w > colWidths[col] { + colWidths[col] = w + } + } + } + + availWidth := func() int { + setWidths := 0 + for col := 0; col < numCols; col++ { + setWidths += colWidths[col] + } + return t.maxWidth - delimSize*(numCols-1) - setWidths + } + numFixedCols := func() int { + fixedCols := 0 + for col := 0; col < numCols; col++ { + if colWidths[col] > 0 { + fixedCols++ + } + } + return fixedCols + } + + // set the widths of short columns + if w := availWidth(); w > 0 { + if numFlexColumns := numCols - numFixedCols(); numFlexColumns > 0 { + perColumn := w / numFlexColumns + for col := 0; col < numCols; col++ { + if max := maxColWidths[col]; max < perColumn { + colWidths[col] = max + } + } + } + } + + // truncate long columns to the remaining available width + if numFlexColumns := numCols - numFixedCols(); numFlexColumns > 0 { + perColumn := availWidth() / numFlexColumns + for col := 0; col < numCols; col++ { + if colWidths[col] == 0 { + if max := maxColWidths[col]; max < perColumn { + colWidths[col] = max + } else if perColumn > 0 { + colWidths[col] = perColumn + } + } + } + } + + // add the remainder to truncated columns + if w := availWidth(); w > 0 { + for col := 0; col < numCols; col++ { + d := maxColWidths[col] - colWidths[col] + toAdd := w + if d < toAdd { + toAdd = d + } + colWidths[col] += toAdd + w -= toAdd + if w <= 0 { + break + } + } + } + + return colWidths +} + +type tsvTablePrinter struct { + out io.Writer + currentCol int +} + +func (t *tsvTablePrinter) AddHeader(_ []string, _ ...fieldOption) {} + +func (t *tsvTablePrinter) AddField(text string, _ ...fieldOption) { + if t.currentCol > 0 { + fmt.Fprint(t.out, "\t") + } + fmt.Fprint(t.out, text) + t.currentCol++ +} + +func (t *tsvTablePrinter) EndRow() { + fmt.Fprint(t.out, "\n") + t.currentCol = 0 +} + +func (t *tsvTablePrinter) Render() error { + return nil +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/template/template.go b/vendor/github.com/cli/go-gh/v2/pkg/template/template.go new file mode 100644 index 000000000..a561ebb65 --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/template/template.go @@ -0,0 +1,261 @@ +// Package template facilitates processing of JSON strings using Go templates. +// Provides additional functions not available using basic Go templates, such as coloring, +// and table rendering. +package template + +import ( + "encoding/json" + "fmt" + "io" + "math" + "strconv" + "strings" + "text/template" + "time" + + "github.com/cli/go-gh/v2/pkg/tableprinter" + "github.com/cli/go-gh/v2/pkg/text" + color "github.com/mgutz/ansi" +) + +const ( + ellipsis = "..." +) + +// Template is the representation of a template. +type Template struct { + colorEnabled bool + output io.Writer + tmpl *template.Template + tp tableprinter.TablePrinter + width int + funcs template.FuncMap +} + +// New initializes a Template. +func New(w io.Writer, width int, colorEnabled bool) *Template { + return &Template{ + colorEnabled: colorEnabled, + output: w, + tp: tableprinter.New(w, true, width), + width: width, + funcs: template.FuncMap{}, + } +} + +// Funcs adds the elements of the argument map to the template's function map. +// It must be called before the template is parsed. +// It is legal to overwrite elements of the map including default functions. +// The return value is the template, so calls can be chained. +func (t *Template) Funcs(funcMap map[string]interface{}) *Template { + for name, f := range funcMap { + t.funcs[name] = f + } + return t +} + +// Parse the given template string for use with Execute. +func (t *Template) Parse(tmpl string) error { + now := time.Now() + templateFuncs := map[string]interface{}{ + "autocolor": colorFunc, + "color": colorFunc, + "hyperlink": hyperlinkFunc, + "join": joinFunc, + "pluck": pluckFunc, + "tablerender": func() (string, error) { + // After rendering a table, prepare a new table printer incase user wants to output + // another table. + defer func() { + t.tp = tableprinter.New(t.output, true, t.width) + }() + return tableRenderFunc(t.tp) + }, + "tablerow": func(fields ...interface{}) (string, error) { + return tableRowFunc(t.tp, fields...) + }, + "timeago": func(input string) (string, error) { + return timeAgoFunc(now, input) + }, + "timefmt": timeFormatFunc, + "truncate": truncateFunc, + } + if !t.colorEnabled { + templateFuncs["autocolor"] = autoColorFunc + } + for name, f := range t.funcs { + templateFuncs[name] = f + } + var err error + t.tmpl, err = template.New("").Funcs(templateFuncs).Parse(tmpl) + return err +} + +// Execute applies the parsed template to the input and writes result to the writer +// the template was initialized with. +func (t *Template) Execute(input io.Reader) error { + jsonData, err := io.ReadAll(input) + if err != nil { + return err + } + + var data interface{} + if err := json.Unmarshal(jsonData, &data); err != nil { + return err + } + + return t.tmpl.Execute(t.output, data) +} + +// Flush writes any remaining data to the writer. This is mostly useful +// when a templates uses the tablerow function but does not include the +// tablerender function at the end. +// If a template did not use the table functionality this is a noop. +func (t *Template) Flush() error { + if _, err := tableRenderFunc(t.tp); err != nil { + return err + } + return nil +} + +func colorFunc(colorName string, input interface{}) (string, error) { + text, err := jsonScalarToString(input) + if err != nil { + return "", err + } + return color.Color(text, colorName), nil +} + +func pluckFunc(field string, input []interface{}) []interface{} { + var results []interface{} + for _, item := range input { + obj := item.(map[string]interface{}) + results = append(results, obj[field]) + } + return results +} + +func joinFunc(sep string, input []interface{}) (string, error) { + var results []string + for _, item := range input { + text, err := jsonScalarToString(item) + if err != nil { + return "", err + } + results = append(results, text) + } + return strings.Join(results, sep), nil +} + +func timeFormatFunc(format, input string) (string, error) { + t, err := time.Parse(time.RFC3339, input) + if err != nil { + return "", err + } + return t.Format(format), nil +} + +func timeAgoFunc(now time.Time, input string) (string, error) { + t, err := time.Parse(time.RFC3339, input) + if err != nil { + return "", err + } + return timeAgo(now.Sub(t)), nil +} + +func truncateFunc(maxWidth int, v interface{}) (string, error) { + if v == nil { + return "", nil + } + if s, ok := v.(string); ok { + return text.Truncate(maxWidth, s), nil + } + return "", fmt.Errorf("invalid value; expected string, got %T", v) +} + +func autoColorFunc(colorName string, input interface{}) (string, error) { + return jsonScalarToString(input) +} + +func tableRowFunc(tp tableprinter.TablePrinter, fields ...interface{}) (string, error) { + if tp == nil { + return "", fmt.Errorf("failed to write table row: no table printer") + } + for _, e := range fields { + s, err := jsonScalarToString(e) + if err != nil { + return "", fmt.Errorf("failed to write table row: %v", err) + } + tp.AddField(s, tableprinter.WithTruncate(truncateMultiline)) + } + tp.EndRow() + return "", nil +} + +func tableRenderFunc(tp tableprinter.TablePrinter) (string, error) { + if tp == nil { + return "", fmt.Errorf("failed to render table: no table printer") + } + err := tp.Render() + if err != nil { + return "", fmt.Errorf("failed to render table: %v", err) + } + return "", nil +} + +func jsonScalarToString(input interface{}) (string, error) { + switch tt := input.(type) { + case string: + return tt, nil + case float64: + if math.Trunc(tt) == tt { + return strconv.FormatFloat(tt, 'f', 0, 64), nil + } else { + return strconv.FormatFloat(tt, 'f', 2, 64), nil + } + case nil: + return "", nil + case bool: + return fmt.Sprintf("%v", tt), nil + default: + return "", fmt.Errorf("cannot convert type to string: %v", tt) + } +} + +func timeAgo(ago time.Duration) string { + if ago < time.Minute { + return "just now" + } + if ago < time.Hour { + return text.Pluralize(int(ago.Minutes()), "minute") + " ago" + } + if ago < 24*time.Hour { + return text.Pluralize(int(ago.Hours()), "hour") + " ago" + } + if ago < 30*24*time.Hour { + return text.Pluralize(int(ago.Hours())/24, "day") + " ago" + } + if ago < 365*24*time.Hour { + return text.Pluralize(int(ago.Hours())/24/30, "month") + " ago" + } + return text.Pluralize(int(ago.Hours()/24/365), "year") + " ago" +} + +// TruncateMultiline returns a copy of the string s that has been shortened to fit the maximum +// display width. If string s has multiple lines the first line will be shortened and all others +// removed. +func truncateMultiline(maxWidth int, s string) string { + if i := strings.IndexAny(s, "\r\n"); i >= 0 { + s = s[:i] + ellipsis + } + return text.Truncate(maxWidth, s) +} + +func hyperlinkFunc(link, text string) string { + if text == "" { + text = link + } + + // See https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda + return fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\", link, text) +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/term/console.go b/vendor/github.com/cli/go-gh/v2/pkg/term/console.go new file mode 100644 index 000000000..4ff7e95d6 --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/term/console.go @@ -0,0 +1,17 @@ +//go:build !windows +// +build !windows + +package term + +import ( + "errors" + "os" +) + +func enableVirtualTerminalProcessing(f *os.File) error { + return errors.New("not implemented") +} + +func openTTY() (*os.File, error) { + return os.Open("/dev/tty") +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/term/console_windows.go b/vendor/github.com/cli/go-gh/v2/pkg/term/console_windows.go new file mode 100644 index 000000000..55b1e42ae --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/term/console_windows.go @@ -0,0 +1,22 @@ +//go:build windows +// +build windows + +package term + +import ( + "os" + + "golang.org/x/sys/windows" +) + +func enableVirtualTerminalProcessing(f *os.File) error { + stdout := windows.Handle(f.Fd()) + + var originalMode uint32 + windows.GetConsoleMode(stdout, &originalMode) + return windows.SetConsoleMode(stdout, originalMode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) +} + +func openTTY() (*os.File, error) { + return os.Open("CONOUT$") +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/term/env.go b/vendor/github.com/cli/go-gh/v2/pkg/term/env.go new file mode 100644 index 000000000..c650cc228 --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/term/env.go @@ -0,0 +1,184 @@ +// Package term provides information about the terminal that the current process is connected to (if any), +// for example measuring the dimensions of the terminal and inspecting whether it's safe to output color. +package term + +import ( + "io" + "os" + "strconv" + "strings" + + "github.com/muesli/termenv" + "golang.org/x/term" +) + +// Term represents information about the terminal that a process is connected to. +type Term struct { + in *os.File + out *os.File + errOut *os.File + isTTY bool + colorEnabled bool + is256enabled bool + hasTrueColor bool + width int + widthPercent int +} + +// FromEnv initializes a Term from [os.Stdout] and environment variables: +// - GH_FORCE_TTY +// - NO_COLOR +// - CLICOLOR +// - CLICOLOR_FORCE +// - TERM +// - COLORTERM +func FromEnv() Term { + var stdoutIsTTY bool + var isColorEnabled bool + var termWidthOverride int + var termWidthPercentage int + + spec := os.Getenv("GH_FORCE_TTY") + if spec != "" { + stdoutIsTTY = true + isColorEnabled = !IsColorDisabled() + + if w, err := strconv.Atoi(spec); err == nil { + termWidthOverride = w + } else if strings.HasSuffix(spec, "%") { + if p, err := strconv.Atoi(spec[:len(spec)-1]); err == nil { + termWidthPercentage = p + } + } + } else { + stdoutIsTTY = IsTerminal(os.Stdout) + isColorEnabled = IsColorForced() || (!IsColorDisabled() && stdoutIsTTY) + } + + isVirtualTerminal := false + if stdoutIsTTY { + if err := enableVirtualTerminalProcessing(os.Stdout); err == nil { + isVirtualTerminal = true + } + } + + return Term{ + in: os.Stdin, + out: os.Stdout, + errOut: os.Stderr, + isTTY: stdoutIsTTY, + colorEnabled: isColorEnabled, + is256enabled: isVirtualTerminal || is256ColorSupported(), + hasTrueColor: isVirtualTerminal || isTrueColorSupported(), + width: termWidthOverride, + widthPercent: termWidthPercentage, + } +} + +// In is the reader reading from standard input. +func (t Term) In() io.Reader { + return t.in +} + +// Out is the writer writing to standard output. +func (t Term) Out() io.Writer { + return t.out +} + +// ErrOut is the writer writing to standard error. +func (t Term) ErrOut() io.Writer { + return t.errOut +} + +// IsTerminalOutput returns true if standard output is connected to a terminal. +func (t Term) IsTerminalOutput() bool { + return t.isTTY +} + +// IsColorEnabled reports whether it's safe to output ANSI color sequences, depending on IsTerminalOutput +// and environment variables. +func (t Term) IsColorEnabled() bool { + return t.colorEnabled +} + +// Is256ColorSupported reports whether the terminal advertises ANSI 256 color codes. +func (t Term) Is256ColorSupported() bool { + return t.is256enabled +} + +// IsTrueColorSupported reports whether the terminal advertises support for ANSI true color sequences. +func (t Term) IsTrueColorSupported() bool { + return t.hasTrueColor +} + +// Size returns the width and height of the terminal that the current process is attached to. +// In case of errors, the numeric values returned are -1. +func (t Term) Size() (int, int, error) { + if t.width > 0 { + return t.width, -1, nil + } + + ttyOut := t.out + if ttyOut == nil || !IsTerminal(ttyOut) { + if f, err := openTTY(); err == nil { + defer f.Close() + ttyOut = f + } else { + return -1, -1, err + } + } + + width, height, err := terminalSize(ttyOut) + if err == nil && t.widthPercent > 0 { + return int(float64(width) * float64(t.widthPercent) / 100), height, nil + } + + return width, height, err +} + +// Theme returns the theme of the terminal by analyzing the background color of the terminal. +func (t Term) Theme() string { + if !t.IsColorEnabled() { + return "none" + } + if termenv.HasDarkBackground() { + return "dark" + } + return "light" +} + +// IsTerminal reports whether a file descriptor is connected to a terminal. +func IsTerminal(f *os.File) bool { + return term.IsTerminal(int(f.Fd())) +} + +func terminalSize(f *os.File) (int, int, error) { + return term.GetSize(int(f.Fd())) +} + +// IsColorDisabled returns true if environment variables NO_COLOR or CLICOLOR prohibit usage of color codes +// in terminal output. +func IsColorDisabled() bool { + return os.Getenv("NO_COLOR") != "" || os.Getenv("CLICOLOR") == "0" +} + +// IsColorForced returns true if environment variable CLICOLOR_FORCE is set to force colored terminal output. +func IsColorForced() bool { + return os.Getenv("CLICOLOR_FORCE") != "" && os.Getenv("CLICOLOR_FORCE") != "0" +} + +func is256ColorSupported() bool { + return isTrueColorSupported() || + strings.Contains(os.Getenv("TERM"), "256") || + strings.Contains(os.Getenv("COLORTERM"), "256") +} + +func isTrueColorSupported() bool { + term := os.Getenv("TERM") + colorterm := os.Getenv("COLORTERM") + + return strings.Contains(term, "24bit") || + strings.Contains(term, "truecolor") || + strings.Contains(colorterm, "24bit") || + strings.Contains(colorterm, "truecolor") +} diff --git a/vendor/github.com/cli/go-gh/v2/pkg/text/text.go b/vendor/github.com/cli/go-gh/v2/pkg/text/text.go new file mode 100644 index 000000000..687517f9d --- /dev/null +++ b/vendor/github.com/cli/go-gh/v2/pkg/text/text.go @@ -0,0 +1,115 @@ +// Package text is a set of utility functions for text processing and outputting to the terminal. +package text + +import ( + "fmt" + "regexp" + "strings" + "time" + "unicode" + + "github.com/charmbracelet/lipgloss" + "github.com/muesli/reflow/truncate" + "golang.org/x/text/runes" + "golang.org/x/text/transform" + "golang.org/x/text/unicode/norm" +) + +const ( + ellipsis = "..." + minWidthForEllipsis = len(ellipsis) + 2 +) + +var indentRE = regexp.MustCompile(`(?m)^`) + +// Indent returns a copy of the string s with indent prefixed to it, will apply indent +// to each line of the string. +func Indent(s, indent string) string { + if len(strings.TrimSpace(s)) == 0 { + return s + } + return indentRE.ReplaceAllLiteralString(s, indent) +} + +// DisplayWidth calculates what the rendered width of string s will be. +func DisplayWidth(s string) int { + return lipgloss.Width(s) +} + +// Truncate returns a copy of the string s that has been shortened to fit the maximum display width. +func Truncate(maxWidth int, s string) string { + w := DisplayWidth(s) + if w <= maxWidth { + return s + } + tail := "" + if maxWidth >= minWidthForEllipsis { + tail = ellipsis + } + r := truncate.StringWithTail(s, uint(maxWidth), tail) + if DisplayWidth(r) < maxWidth { + r += " " + } + return r +} + +// PadRight returns a copy of the string s that has been padded on the right with whitespace to fit +// the maximum display width. +func PadRight(maxWidth int, s string) string { + if padWidth := maxWidth - DisplayWidth(s); padWidth > 0 { + s += strings.Repeat(" ", padWidth) + } + return s +} + +// Pluralize returns a concatenated string with num and the plural form of thing if necessary. +func Pluralize(num int, thing string) string { + if num == 1 { + return fmt.Sprintf("%d %s", num, thing) + } + return fmt.Sprintf("%d %ss", num, thing) +} + +func fmtDuration(amount int, unit string) string { + return fmt.Sprintf("about %s ago", Pluralize(amount, unit)) +} + +// RelativeTimeAgo returns a human readable string of the time duration between a and b that is estimated +// to the nearest unit of time. +func RelativeTimeAgo(a, b time.Time) string { + ago := a.Sub(b) + + if ago < time.Minute { + return "less than a minute ago" + } + if ago < time.Hour { + return fmtDuration(int(ago.Minutes()), "minute") + } + if ago < 24*time.Hour { + return fmtDuration(int(ago.Hours()), "hour") + } + if ago < 30*24*time.Hour { + return fmtDuration(int(ago.Hours())/24, "day") + } + if ago < 365*24*time.Hour { + return fmtDuration(int(ago.Hours())/24/30, "month") + } + + return fmtDuration(int(ago.Hours()/24/365), "year") +} + +// RemoveDiacritics returns the input value without "diacritics", or accent marks. +func RemoveDiacritics(value string) string { + // Mn = "Mark, nonspacing" unicode character category + removeMnTransfomer := runes.Remove(runes.In(unicode.Mn)) + + // 1. Decompose the text into characters and diacritical marks + // 2. Remove the diacriticals marks + // 3. Recompose the text + t := transform.Chain(norm.NFD, removeMnTransfomer, norm.NFC) + normalized, _, err := transform.String(t, value) + if err != nil { + return value + } + return normalized +} diff --git a/vendor/github.com/cli/oauth/.golangci.yml b/vendor/github.com/cli/oauth/.golangci.yml new file mode 100644 index 000000000..aa52e9842 --- /dev/null +++ b/vendor/github.com/cli/oauth/.golangci.yml @@ -0,0 +1,15 @@ +linters: + enable: + - gofmt + - godot + - revive + +linters-settings: + godot: + # comments to be checked: `declarations`, `toplevel`, or `all` + scope: declarations + # check that each sentence starts with a capital letter + capital: true + +issues: + exclude-use-default: false diff --git a/vendor/github.com/cli/oauth/LICENSE b/vendor/github.com/cli/oauth/LICENSE new file mode 100644 index 000000000..284b811ef --- /dev/null +++ b/vendor/github.com/cli/oauth/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 GitHub, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/cli/oauth/README.md b/vendor/github.com/cli/oauth/README.md new file mode 100644 index 000000000..41cb35c77 --- /dev/null +++ b/vendor/github.com/cli/oauth/README.md @@ -0,0 +1,28 @@ +# oauth + +A library for Go client applications that need to perform OAuth authorization against a server, typically GitHub.com. + +

+
+ +

+ +Traditionally, OAuth for web applications involves redirecting to a URI after the user authorizes an app. While web apps (and some native client apps) can receive a browser redirect, client apps such as CLI applications do not have such an option. + +To accommodate client apps, this library implements the [OAuth Device Authorization Grant][oauth-device] which [GitHub.com now supports][gh-device]. With Device flow, the user is presented with a one-time code that they will have to enter in a web browser while authorizing the app on the server. Device flow is suitable for cases where the web browser may be running on a separate device than the client app itself; for example a CLI application could run within a headless, containerized instance, but the user may complete authorization using a browser on their phone. + +To transparently enable OAuth authorization on _any GitHub host_ (e.g. GHES instances without OAuth “Device flow” support), this library also bundles an implementation of OAuth web application flow in which the client app starts a local server at `http://127.0.0.1:/` that acts as a receiver for the browser redirect. First, Device flow is attempted, and the localhost server is used as fallback. With the localhost server, the user's web browser must be running on the same machine as the client application itself. + +## Usage + +- [OAuth Device flow with fallback](./examples_test.go) +- [manual OAuth Device flow](./device/examples_test.go) +- [manual OAuth web application flow](./webapp/examples_test.go) + +Applications that need more control over the user experience around authentication should directly interface with `github.com/cli/oauth/device` and `github.com/cli/oauth/webapp` packages. + +In theory, these packages would enable authorization on any OAuth-enabled host. In practice, however, this was only tested for authorizing with GitHub. + + +[oauth-device]: https://oauth.net/2/device-flow/ +[gh-device]: https://docs.github.com/en/free-pro-team@latest/developers/apps/authorizing-oauth-apps#device-flow diff --git a/vendor/github.com/cli/oauth/api/access_token.go b/vendor/github.com/cli/oauth/api/access_token.go new file mode 100644 index 000000000..718d69d50 --- /dev/null +++ b/vendor/github.com/cli/oauth/api/access_token.go @@ -0,0 +1,27 @@ +package api + +// AccessToken is an OAuth access token. +type AccessToken struct { + // The token value, typically a 40-character random string. + Token string + // The refresh token value, associated with the access token. + RefreshToken string + // The token type, e.g. "bearer". + Type string + // Space-separated list of OAuth scopes that this token grants. + Scope string +} + +// AccessToken extracts the access token information from a server response. +func (f FormResponse) AccessToken() (*AccessToken, error) { + if accessToken := f.Get("access_token"); accessToken != "" { + return &AccessToken{ + Token: accessToken, + RefreshToken: f.Get("refresh_token"), + Type: f.Get("token_type"), + Scope: f.Get("scope"), + }, nil + } + + return nil, f.Err() +} diff --git a/vendor/github.com/cli/oauth/api/form.go b/vendor/github.com/cli/oauth/api/form.go new file mode 100644 index 000000000..9b28484ed --- /dev/null +++ b/vendor/github.com/cli/oauth/api/form.go @@ -0,0 +1,115 @@ +// Package api implements request and response parsing logic shared between different OAuth strategies. +package api + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "mime" + "net/http" + "net/url" + "strconv" +) + +type httpClient interface { + PostForm(string, url.Values) (*http.Response, error) +} + +// FormResponse is the parsed "www-form-urlencoded" response from the server. +type FormResponse struct { + StatusCode int + + requestURI string + values url.Values +} + +// Get the response value named k. +func (f FormResponse) Get(k string) string { + return f.values.Get(k) +} + +// Err returns an Error object extracted from the response. +func (f FormResponse) Err() error { + return &Error{ + RequestURI: f.requestURI, + ResponseCode: f.StatusCode, + Code: f.Get("error"), + message: f.Get("error_description"), + } +} + +// Error is the result of an unexpected HTTP response from the server. +type Error struct { + Code string + ResponseCode int + RequestURI string + + message string +} + +func (e Error) Error() string { + if e.message != "" { + return fmt.Sprintf("%s (%s)", e.message, e.Code) + } + if e.Code != "" { + return e.Code + } + return fmt.Sprintf("HTTP %d", e.ResponseCode) +} + +// PostForm makes an POST request by serializing input parameters as a form and parsing the response +// of the same type. +func PostForm(c httpClient, u string, params url.Values) (*FormResponse, error) { + resp, err := c.PostForm(u, params) + if err != nil { + return nil, err + } + defer func() { + _ = resp.Body.Close() + }() + + r := &FormResponse{ + StatusCode: resp.StatusCode, + requestURI: u, + } + + mediaType, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")) + switch mediaType { + case "application/x-www-form-urlencoded": + var bb []byte + bb, err = ioutil.ReadAll(resp.Body) + if err != nil { + return r, err + } + + r.values, err = url.ParseQuery(string(bb)) + if err != nil { + return r, err + } + case "application/json": + var values map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&values); err != nil { + return r, err + } + + r.values = make(url.Values) + for key, value := range values { + switch v := value.(type) { + case string: + r.values.Set(key, v) + case int64: + r.values.Set(key, strconv.FormatInt(v, 10)) + case float64: + r.values.Set(key, strconv.FormatFloat(v, 'f', -1, 64)) + } + } + default: + _, err = io.Copy(ioutil.Discard, resp.Body) + if err != nil { + return r, err + } + } + + return r, nil +} diff --git a/vendor/github.com/cli/oauth/device/device_flow.go b/vendor/github.com/cli/oauth/device/device_flow.go new file mode 100644 index 000000000..e6a6ee95c --- /dev/null +++ b/vendor/github.com/cli/oauth/device/device_flow.go @@ -0,0 +1,175 @@ +// Package device facilitates performing OAuth Device Authorization Flow for client applications +// such as CLIs that can not receive redirects from a web site. +// +// First, RequestCode should be used to obtain a CodeResponse. +// +// Next, the user will need to navigate to VerificationURI in their web browser on any device and fill +// in the UserCode. +// +// While the user is completing the web flow, the application should invoke PollToken, which blocks +// the goroutine until the user has authorized the app on the server. +// +// https://docs.github.com/en/free-pro-team@latest/developers/apps/authorizing-oauth-apps#device-flow +package device + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/cli/oauth/api" +) + +var ( + // ErrUnsupported is thrown when the server does not implement Device flow. + ErrUnsupported = errors.New("device flow not supported") + // ErrTimeout is thrown when polling the server for the granted token has timed out. + ErrTimeout = errors.New("authentication timed out") +) + +type httpClient interface { + PostForm(string, url.Values) (*http.Response, error) +} + +// CodeResponse holds information about the authorization-in-progress. +type CodeResponse struct { + // The user verification code is displayed on the device so the user can enter the code in a browser. + UserCode string + // The verification URL where users need to enter the UserCode. + VerificationURI string + // The optional verification URL that includes the UserCode. + VerificationURIComplete string + + // The device verification code is 40 characters and used to verify the device. + DeviceCode string + // The number of seconds before the DeviceCode and UserCode expire. + ExpiresIn int + // The minimum number of seconds that must pass before you can make a new access token request to + // complete the device authorization. + Interval int +} + +// RequestCode initiates the authorization flow by requesting a code from uri. +func RequestCode(c httpClient, uri string, clientID string, scopes []string) (*CodeResponse, error) { + resp, err := api.PostForm(c, uri, url.Values{ + "client_id": {clientID}, + "scope": {strings.Join(scopes, " ")}, + }) + if err != nil { + return nil, err + } + + verificationURI := resp.Get("verification_uri") + if verificationURI == "" { + // Google's "OAuth 2.0 for TV and Limited-Input Device Applications" uses `verification_url`. + verificationURI = resp.Get("verification_url") + } + + if resp.StatusCode == 401 || resp.StatusCode == 403 || resp.StatusCode == 404 || resp.StatusCode == 422 || + (resp.StatusCode == 200 && verificationURI == "") || + (resp.StatusCode == 400 && resp.Get("error") == "device_flow_disabled") || + (resp.StatusCode == 400 && resp.Get("error") == "unauthorized_client") { + return nil, ErrUnsupported + } + + if resp.StatusCode != 200 { + return nil, resp.Err() + } + + intervalSeconds, err := strconv.Atoi(resp.Get("interval")) + if err != nil { + return nil, fmt.Errorf("could not parse interval=%q as integer: %w", resp.Get("interval"), err) + } + + expiresIn, err := strconv.Atoi(resp.Get("expires_in")) + if err != nil { + return nil, fmt.Errorf("could not parse expires_in=%q as integer: %w", resp.Get("expires_in"), err) + } + + return &CodeResponse{ + DeviceCode: resp.Get("device_code"), + UserCode: resp.Get("user_code"), + VerificationURI: verificationURI, + VerificationURIComplete: resp.Get("verification_uri_complete"), + Interval: intervalSeconds, + ExpiresIn: expiresIn, + }, nil +} + +const defaultGrantType = "urn:ietf:params:oauth:grant-type:device_code" + +// PollToken polls the server at pollURL until an access token is granted or denied. +// +// Deprecated: use Wait. +func PollToken(c httpClient, pollURL string, clientID string, code *CodeResponse) (*api.AccessToken, error) { + return Wait(context.Background(), c, pollURL, WaitOptions{ + ClientID: clientID, + DeviceCode: code, + }) +} + +// WaitOptions specifies parameters to poll the server with until authentication completes. +type WaitOptions struct { + // ClientID is the app client ID value. + ClientID string + // ClientSecret is the app client secret value. Optional: only pass if the server requires it. + ClientSecret string + // DeviceCode is the value obtained from RequestCode. + DeviceCode *CodeResponse + // GrantType overrides the default value specified by OAuth 2.0 Device Code. Optional. + GrantType string + + newPoller pollerFactory +} + +// Wait polls the server at uri until authorization completes. +func Wait(ctx context.Context, c httpClient, uri string, opts WaitOptions) (*api.AccessToken, error) { + checkInterval := time.Duration(opts.DeviceCode.Interval) * time.Second + expiresIn := time.Duration(opts.DeviceCode.ExpiresIn) * time.Second + grantType := opts.GrantType + if opts.GrantType == "" { + grantType = defaultGrantType + } + + makePoller := opts.newPoller + if makePoller == nil { + makePoller = newPoller + } + _, poll := makePoller(ctx, checkInterval, expiresIn) + + for { + if err := poll.Wait(); err != nil { + return nil, err + } + + values := url.Values{ + "client_id": {opts.ClientID}, + "device_code": {opts.DeviceCode.DeviceCode}, + "grant_type": {grantType}, + } + + // Google's "OAuth 2.0 for TV and Limited-Input Device Applications" requires `client_secret`. + if opts.ClientSecret != "" { + values.Add("client_secret", opts.ClientSecret) + } + + // TODO: pass tctx down to the HTTP layer + resp, err := api.PostForm(c, uri, values) + if err != nil { + return nil, err + } + + var apiError *api.Error + token, err := resp.AccessToken() + if err == nil { + return token, nil + } else if !(errors.As(err, &apiError) && apiError.Code == "authorization_pending") { + return nil, err + } + } +} diff --git a/vendor/github.com/cli/oauth/device/poller.go b/vendor/github.com/cli/oauth/device/poller.go new file mode 100644 index 000000000..06a2e9d2c --- /dev/null +++ b/vendor/github.com/cli/oauth/device/poller.go @@ -0,0 +1,43 @@ +package device + +import ( + "context" + "time" +) + +type poller interface { + Wait() error + Cancel() +} + +type pollerFactory func(context.Context, time.Duration, time.Duration) (context.Context, poller) + +func newPoller(ctx context.Context, checkInteval, expiresIn time.Duration) (context.Context, poller) { + c, cancel := context.WithTimeout(ctx, expiresIn) + return c, &intervalPoller{ + ctx: c, + interval: checkInteval, + cancelFunc: cancel, + } +} + +type intervalPoller struct { + ctx context.Context + interval time.Duration + cancelFunc func() +} + +func (p intervalPoller) Wait() error { + t := time.NewTimer(p.interval) + select { + case <-p.ctx.Done(): + t.Stop() + return p.ctx.Err() + case <-t.C: + return nil + } +} + +func (p intervalPoller) Cancel() { + p.cancelFunc() +} diff --git a/vendor/github.com/cli/oauth/oauth.go b/vendor/github.com/cli/oauth/oauth.go new file mode 100644 index 000000000..643978212 --- /dev/null +++ b/vendor/github.com/cli/oauth/oauth.go @@ -0,0 +1,79 @@ +// Package oauth is a library for Go client applications that need to perform OAuth authorization +// against a server, typically GitHub.com. +package oauth + +import ( + "errors" + "fmt" + "io" + "net/http" + "net/url" + + "github.com/cli/oauth/api" + "github.com/cli/oauth/device" +) + +type httpClient interface { + PostForm(string, url.Values) (*http.Response, error) +} + +// Host defines the endpoints used to authorize against an OAuth server. +type Host struct { + DeviceCodeURL string + AuthorizeURL string + TokenURL string +} + +// GitHubHost constructs a Host from the given URL to a GitHub instance. +func GitHubHost(hostURL string) *Host { + u, _ := url.Parse(hostURL) + + return &Host{ + DeviceCodeURL: fmt.Sprintf("%s://%s/login/device/code", u.Scheme, u.Host), + AuthorizeURL: fmt.Sprintf("%s://%s/login/oauth/authorize", u.Scheme, u.Host), + TokenURL: fmt.Sprintf("%s://%s/login/oauth/access_token", u.Scheme, u.Host), + } +} + +// Flow facilitates a single OAuth authorization flow. +type Flow struct { + // The hostname to authorize the app with. + // + // Deprecated: Use Host instead. + Hostname string + // Host configuration to authorize the app with. + Host *Host + // OAuth scopes to request from the user. + Scopes []string + // OAuth application ID. + ClientID string + // OAuth application secret. Only applicable in web application flow. + ClientSecret string + // The localhost URI for web application flow callback, e.g. "http://127.0.0.1/callback". + CallbackURI string + + // Display a one-time code to the user. Receives the code and the browser URL as arguments. Defaults to printing the + // code to the user on Stdout with instructions to copy the code and to press Enter to continue in their browser. + DisplayCode func(string, string) error + // Open a web browser at a URL. Defaults to opening the default system browser. + BrowseURL func(string) error + // Render an HTML page to the user upon completion of web application flow. The default is to + // render a simple message that informs the user they can close the browser tab and return to the app. + WriteSuccessHTML func(io.Writer) + + // The HTTP client to use for API POST requests. Defaults to http.DefaultClient. + HTTPClient httpClient + // The stream to listen to keyboard input on. Defaults to os.Stdin. + Stdin io.Reader + // The stream to print UI messages to. Defaults to os.Stdout. + Stdout io.Writer +} + +// DetectFlow tries to perform Device flow first and falls back to Web application flow. +func (oa *Flow) DetectFlow() (*api.AccessToken, error) { + accessToken, err := oa.DeviceFlow() + if errors.Is(err, device.ErrUnsupported) { + return oa.WebAppFlow() + } + return accessToken, err +} diff --git a/vendor/github.com/cli/oauth/oauth_device.go b/vendor/github.com/cli/oauth/oauth_device.go new file mode 100644 index 000000000..d4615ef2a --- /dev/null +++ b/vendor/github.com/cli/oauth/oauth_device.go @@ -0,0 +1,72 @@ +package oauth + +import ( + "bufio" + "context" + "fmt" + "io" + "net/http" + "os" + + "github.com/cli/browser" + "github.com/cli/oauth/api" + "github.com/cli/oauth/device" +) + +// DeviceFlow captures the full OAuth Device flow, including prompting the user to copy a one-time +// code and opening their web browser, and returns an access token upon completion. +func (oa *Flow) DeviceFlow() (*api.AccessToken, error) { + httpClient := oa.HTTPClient + if httpClient == nil { + httpClient = http.DefaultClient + } + + stdin := oa.Stdin + if stdin == nil { + stdin = os.Stdin + } + stdout := oa.Stdout + if stdout == nil { + stdout = os.Stdout + } + host := oa.Host + if host == nil { + host = GitHubHost("https://" + oa.Hostname) + } + + code, err := device.RequestCode(httpClient, host.DeviceCodeURL, oa.ClientID, oa.Scopes) + if err != nil { + return nil, err + } + + if oa.DisplayCode == nil { + fmt.Fprintf(stdout, "First, copy your one-time code: %s\n", code.UserCode) + fmt.Fprint(stdout, "Then press [Enter] to continue in the web browser... ") + _ = waitForEnter(stdin) + } else { + err := oa.DisplayCode(code.UserCode, code.VerificationURI) + if err != nil { + return nil, err + } + } + + browseURL := oa.BrowseURL + if browseURL == nil { + browseURL = browser.OpenURL + } + + if err = browseURL(code.VerificationURI); err != nil { + return nil, fmt.Errorf("error opening the web browser: %w", err) + } + + return device.Wait(context.TODO(), httpClient, host.TokenURL, device.WaitOptions{ + ClientID: oa.ClientID, + DeviceCode: code, + }) +} + +func waitForEnter(r io.Reader) error { + scanner := bufio.NewScanner(r) + scanner.Scan() + return scanner.Err() +} diff --git a/vendor/github.com/cli/oauth/oauth_webapp.go b/vendor/github.com/cli/oauth/oauth_webapp.go new file mode 100644 index 000000000..e448715bc --- /dev/null +++ b/vendor/github.com/cli/oauth/oauth_webapp.go @@ -0,0 +1,59 @@ +package oauth + +import ( + "context" + "fmt" + "net/http" + + "github.com/cli/browser" + "github.com/cli/oauth/api" + "github.com/cli/oauth/webapp" +) + +// WebAppFlow starts a local HTTP server, opens the web browser to initiate the OAuth Web application +// flow, blocks until the user completes authorization and is redirected back, and returns the access token. +func (oa *Flow) WebAppFlow() (*api.AccessToken, error) { + host := oa.Host + if host == nil { + host = GitHubHost("https://" + oa.Hostname) + } + + flow, err := webapp.InitFlow() + if err != nil { + return nil, err + } + + params := webapp.BrowserParams{ + ClientID: oa.ClientID, + RedirectURI: oa.CallbackURI, + Scopes: oa.Scopes, + AllowSignup: true, + } + browserURL, err := flow.BrowserURL(host.AuthorizeURL, params) + if err != nil { + return nil, err + } + + go func() { + _ = flow.StartServer(oa.WriteSuccessHTML) + }() + + browseURL := oa.BrowseURL + if browseURL == nil { + browseURL = browser.OpenURL + } + + err = browseURL(browserURL) + if err != nil { + return nil, fmt.Errorf("error opening the web browser: %w", err) + } + + httpClient := oa.HTTPClient + if httpClient == nil { + httpClient = http.DefaultClient + } + + return flow.Wait(context.TODO(), httpClient, host.TokenURL, webapp.WaitOptions{ + ClientSecret: oa.ClientSecret, + }) +} diff --git a/vendor/github.com/cli/oauth/webapp/local_server.go b/vendor/github.com/cli/oauth/webapp/local_server.go new file mode 100644 index 000000000..c895dfe02 --- /dev/null +++ b/vendor/github.com/cli/oauth/webapp/local_server.go @@ -0,0 +1,85 @@ +package webapp + +import ( + "context" + "fmt" + "io" + "net" + "net/http" +) + +// CodeResponse represents the code received by the local server's callback handler. +type CodeResponse struct { + Code string + State string +} + +// bindLocalServer initializes a LocalServer that will listen on a randomly available TCP port. +func bindLocalServer() (*localServer, error) { + listener, err := net.Listen("tcp4", "127.0.0.1:0") + if err != nil { + return nil, err + } + + return &localServer{ + listener: listener, + resultChan: make(chan CodeResponse, 1), + }, nil +} + +type localServer struct { + CallbackPath string + WriteSuccessHTML func(w io.Writer) + + resultChan chan (CodeResponse) + listener net.Listener +} + +func (s *localServer) Port() int { + return s.listener.Addr().(*net.TCPAddr).Port +} + +func (s *localServer) Close() error { + return s.listener.Close() +} + +func (s *localServer) Serve() error { + return http.Serve(s.listener, s) +} + +func (s *localServer) WaitForCode(ctx context.Context) (CodeResponse, error) { + select { + case <-ctx.Done(): + return CodeResponse{}, ctx.Err() + case code := <-s.resultChan: + return code, nil + } +} + +// ServeHTTP implements http.Handler. +func (s *localServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if s.CallbackPath != "" && r.URL.Path != s.CallbackPath { + w.WriteHeader(404) + return + } + defer func() { + _ = s.Close() + }() + + params := r.URL.Query() + s.resultChan <- CodeResponse{ + Code: params.Get("code"), + State: params.Get("state"), + } + + w.Header().Add("content-type", "text/html") + if s.WriteSuccessHTML != nil { + s.WriteSuccessHTML(w) + } else { + defaultSuccessHTML(w) + } +} + +func defaultSuccessHTML(w io.Writer) { + fmt.Fprintf(w, "

You may now close this page and return to the client app.

") +} diff --git a/vendor/github.com/cli/oauth/webapp/webapp_flow.go b/vendor/github.com/cli/oauth/webapp/webapp_flow.go new file mode 100644 index 000000000..49c655106 --- /dev/null +++ b/vendor/github.com/cli/oauth/webapp/webapp_flow.go @@ -0,0 +1,132 @@ +// Package webapp implements the OAuth Web Application authorization flow for client applications by +// starting a server at localhost to receive the web redirect after the user has authorized the application. +package webapp + +import ( + "context" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/cli/oauth/api" +) + +type httpClient interface { + PostForm(string, url.Values) (*http.Response, error) +} + +// Flow holds the state for the steps of OAuth Web Application flow. +type Flow struct { + server *localServer + clientID string + state string +} + +// InitFlow creates a new Flow instance by detecting a locally available port number. +func InitFlow() (*Flow, error) { + server, err := bindLocalServer() + if err != nil { + return nil, err + } + + state, _ := randomString(20) + + return &Flow{ + server: server, + state: state, + }, nil +} + +// BrowserParams are GET query parameters for initiating the web flow. +type BrowserParams struct { + ClientID string + RedirectURI string + Scopes []string + LoginHandle string + AllowSignup bool +} + +// BrowserURL appends GET query parameters to baseURL and returns the url that the user should +// navigate to in their web browser. +func (flow *Flow) BrowserURL(baseURL string, params BrowserParams) (string, error) { + ru, err := url.Parse(params.RedirectURI) + if err != nil { + return "", err + } + + ru.Host = fmt.Sprintf("%s:%d", ru.Hostname(), flow.server.Port()) + flow.server.CallbackPath = ru.Path + flow.clientID = params.ClientID + + q := url.Values{} + q.Set("client_id", params.ClientID) + q.Set("redirect_uri", ru.String()) + q.Set("scope", strings.Join(params.Scopes, " ")) + q.Set("state", flow.state) + if params.LoginHandle != "" { + q.Set("login", params.LoginHandle) + } + if !params.AllowSignup { + q.Set("allow_signup", "false") + } + + return fmt.Sprintf("%s?%s", baseURL, q.Encode()), nil +} + +// StartServer starts the localhost server and blocks until it has received the web redirect. The +// writeSuccess function can be used to render a HTML page to the user upon completion. +func (flow *Flow) StartServer(writeSuccess func(io.Writer)) error { + flow.server.WriteSuccessHTML = writeSuccess + return flow.server.Serve() +} + +// AccessToken blocks until the browser flow has completed and returns the access token. +// +// Deprecated: use Wait. +func (flow *Flow) AccessToken(c httpClient, tokenURL, clientSecret string) (*api.AccessToken, error) { + return flow.Wait(context.Background(), c, tokenURL, WaitOptions{ClientSecret: clientSecret}) +} + +// WaitOptions specifies parameters to exchange the access token for. +type WaitOptions struct { + // ClientSecret is the app client secret value. + ClientSecret string +} + +// Wait blocks until the browser flow has completed and returns the access token. +func (flow *Flow) Wait(ctx context.Context, c httpClient, tokenURL string, opts WaitOptions) (*api.AccessToken, error) { + code, err := flow.server.WaitForCode(ctx) + if err != nil { + return nil, err + } + if code.State != flow.state { + return nil, errors.New("state mismatch") + } + + resp, err := api.PostForm(c, tokenURL, + url.Values{ + "client_id": {flow.clientID}, + "client_secret": {opts.ClientSecret}, + "code": {code.Code}, + "state": {flow.state}, + }) + if err != nil { + return nil, err + } + + return resp.AccessToken() +} + +func randomString(length int) (string, error) { + b := make([]byte, length/2) + _, err := rand.Read(b) + if err != nil { + return "", err + } + return hex.EncodeToString(b), nil +} diff --git a/vendor/github.com/cli/safeexec/LICENSE b/vendor/github.com/cli/safeexec/LICENSE new file mode 100644 index 000000000..ca498575a --- /dev/null +++ b/vendor/github.com/cli/safeexec/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2020, GitHub Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/cli/safeexec/README.md b/vendor/github.com/cli/safeexec/README.md new file mode 100644 index 000000000..4ff1c2aca --- /dev/null +++ b/vendor/github.com/cli/safeexec/README.md @@ -0,0 +1,48 @@ +# safeexec + +A Go module that provides a stabler alternative to `exec.LookPath()` that: +- Avoids a Windows security risk of executing commands found in the current directory; and +- Allows executing commands found in PATH, even if they come from relative PATH entries. + +This is an alternative to [`golang.org/x/sys/execabs`](https://pkg.go.dev/golang.org/x/sys/execabs). + +## Usage +```go +import ( + "os/exec" + "github.com/cli/safeexec" +) + +func gitStatus() error { + gitBin, err := safeexec.LookPath("git") + if err != nil { + return err + } + cmd := exec.Command(gitBin, "status") + return cmd.Run() +} +``` + +## Background +### Windows security vulnerability with Go <= 1.18 +Go 1.18 (and older) standard library has a security vulnerability when executing programs: +```go +import "os/exec" + +func gitStatus() error { + // On Windows, this will result in `.\git.exe` or `.\git.bat` being executed + // if either were found in the current working directory. + cmd := exec.Command("git", "status") + return cmd.Run() +} +``` + +For historic reasons, Go used to implicitly [include the current directory](https://github.com/golang/go/issues/38736) in the PATH resolution on Windows. The `safeexec` package avoids searching the current directory on Windows. + +### Relative PATH entries with Go 1.19+ + +Go 1.19 (and newer) standard library [throws an error](https://github.com/golang/go/issues/43724) if `exec.LookPath("git")` resolved to an executable relative to the current directory. This can happen on other platforms if the PATH environment variable contains relative entries, e.g. `PATH=./bin:$PATH`. The `safeexec` package allows respecting relative PATH entries as it assumes that the responsibility for keeping PATH safe lies outside of the Go program. + +## TODO + +Ideally, this module would also provide `exec.Command()` and `exec.CommandContext()` equivalents that delegate to the patched version of `LookPath`. However, this doesn't seem possible since `LookPath` may return an error, while `exec.Command/CommandContext()` themselves do not return an error. In the standard library, the resulting `exec.Cmd` struct stores the LookPath error in a private field, but that functionality isn't available to us. diff --git a/vendor/github.com/cli/safeexec/lookpath.go b/vendor/github.com/cli/safeexec/lookpath.go new file mode 100644 index 000000000..e649ca7d1 --- /dev/null +++ b/vendor/github.com/cli/safeexec/lookpath.go @@ -0,0 +1,17 @@ +//go:build !windows && go1.19 +// +build !windows,go1.19 + +package safeexec + +import ( + "errors" + "os/exec" +) + +func LookPath(file string) (string, error) { + path, err := exec.LookPath(file) + if errors.Is(err, exec.ErrDot) { + return path, nil + } + return path, err +} diff --git a/vendor/github.com/cli/safeexec/lookpath_1.18.go b/vendor/github.com/cli/safeexec/lookpath_1.18.go new file mode 100644 index 000000000..bb4a27e4f --- /dev/null +++ b/vendor/github.com/cli/safeexec/lookpath_1.18.go @@ -0,0 +1,10 @@ +//go:build !windows && !go1.19 +// +build !windows,!go1.19 + +package safeexec + +import "os/exec" + +func LookPath(file string) (string, error) { + return exec.LookPath(file) +} diff --git a/vendor/github.com/cli/safeexec/lookpath_windows.go b/vendor/github.com/cli/safeexec/lookpath_windows.go new file mode 100644 index 000000000..19b3e52f7 --- /dev/null +++ b/vendor/github.com/cli/safeexec/lookpath_windows.go @@ -0,0 +1,120 @@ +// Copyright (c) 2009 The Go Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Package safeexec provides alternatives for exec package functions to avoid +// accidentally executing binaries found in the current working directory on +// Windows. +package safeexec + +import ( + "os" + "os/exec" + "path/filepath" + "strings" +) + +func chkStat(file string) error { + d, err := os.Stat(file) + if err != nil { + return err + } + if d.IsDir() { + return os.ErrPermission + } + return nil +} + +func hasExt(file string) bool { + i := strings.LastIndex(file, ".") + if i < 0 { + return false + } + return strings.LastIndexAny(file, `:\/`) < i +} + +func findExecutable(file string, exts []string) (string, error) { + if len(exts) == 0 { + return file, chkStat(file) + } + if hasExt(file) { + if chkStat(file) == nil { + return file, nil + } + } + for _, e := range exts { + if f := file + e; chkStat(f) == nil { + return f, nil + } + } + return "", os.ErrNotExist +} + +// LookPath searches for an executable named file in the +// directories named by the PATH environment variable. +// If file contains a slash, it is tried directly and the PATH is not consulted. +// LookPath also uses PATHEXT environment variable to match +// a suitable candidate. +// The result may be an absolute path or a path relative to the current directory. +func LookPath(file string) (string, error) { + var exts []string + x := os.Getenv(`PATHEXT`) + if x != "" { + for _, e := range strings.Split(strings.ToLower(x), `;`) { + if e == "" { + continue + } + if e[0] != '.' { + e = "." + e + } + exts = append(exts, e) + } + } else { + exts = []string{".com", ".exe", ".bat", ".cmd"} + } + + if strings.ContainsAny(file, `:\/`) { + if f, err := findExecutable(file, exts); err == nil { + return f, nil + } else { + return "", &exec.Error{file, err} + } + } + + // https://github.com/golang/go/issues/38736 + // if f, err := findExecutable(filepath.Join(".", file), exts); err == nil { + // return f, nil + // } + + path := os.Getenv("path") + for _, dir := range filepath.SplitList(path) { + if f, err := findExecutable(filepath.Join(dir, file), exts); err == nil { + return f, nil + } + } + return "", &exec.Error{file, exec.ErrNotFound} +} diff --git a/vendor/github.com/cli/shurcooL-graphql/.golangci.yml b/vendor/github.com/cli/shurcooL-graphql/.golangci.yml new file mode 100644 index 000000000..2d5cc27b6 --- /dev/null +++ b/vendor/github.com/cli/shurcooL-graphql/.golangci.yml @@ -0,0 +1,11 @@ +linters: + enable: + - gofmt + - godot + +linters-settings: + godot: + # comments to be checked: `declarations`, `toplevel`, or `all` + scope: declarations + # check that each sentence starts with a capital letter + capital: true diff --git a/vendor/github.com/cli/shurcooL-graphql/LICENSE b/vendor/github.com/cli/shurcooL-graphql/LICENSE new file mode 100644 index 000000000..ca4c77642 --- /dev/null +++ b/vendor/github.com/cli/shurcooL-graphql/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Dmitri Shuralyov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/cli/shurcooL-graphql/README.md b/vendor/github.com/cli/shurcooL-graphql/README.md new file mode 100644 index 000000000..a49c52474 --- /dev/null +++ b/vendor/github.com/cli/shurcooL-graphql/README.md @@ -0,0 +1,276 @@ +graphql +======= + +Package `graphql` provides a GraphQL client implementation, and is forked from `https://github.com/shurcooL/graphql`. + +Installation +------------ + +```bash +go get -u github.com/cli/shurcooL-graphql +``` + +Usage +----- + +Construct a GraphQL client, specifying the GraphQL server URL. Then, you can use it to make GraphQL queries and mutations. + +```Go +client := graphql.NewClient("https://example.com/graphql", nil) +// Use client... +``` + +### Authentication + +Some GraphQL servers may require authentication. The `graphql` package does not directly handle authentication. Instead, when creating a new client, you're expected to pass an `http.Client` that performs authentication. The easiest and recommended way to do this is to use the [`golang.org/x/oauth2`](https://golang.org/x/oauth2) package. You'll need an OAuth token with the right scopes. Then: + +```Go +import "golang.org/x/oauth2" + +func main() { + src := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: os.Getenv("GRAPHQL_TOKEN")}, + ) + httpClient := oauth2.NewClient(context.Background(), src) + + client := graphql.NewClient("https://example.com/graphql", httpClient) + // Use client... +``` + +### Simple Query + +To make a GraphQL query, you need to define a corresponding Go type. + +For example, to make the following GraphQL query: + +```GraphQL +query { + me { + name + } +} +``` + +You can define this variable: + +```Go +var query struct { + Me struct { + Name graphql.String + } +} +``` + +Then call `client.Query`, passing a pointer to it: + +```Go +err := client.Query(context.Background(), &query, nil) +if err != nil { + // Handle error. +} +fmt.Println(query.Me.Name) + +// Output: Luke Skywalker +``` + +### Arguments and Variables + +Often, you'll want to specify arguments on some fields. You can use the `graphql` struct field tag for this. + +For example, to make the following GraphQL query: + +```GraphQL +{ + human(id: "1000") { + name + height(unit: METER) + } +} +``` + +You can define this variable: + +```Go +var q struct { + Human struct { + Name graphql.String + Height graphql.Float `graphql:"height(unit: METER)"` + } `graphql:"human(id: \"1000\")"` +} +``` + +Then call `client.Query`: + +```Go +err := client.Query(context.Background(), &q, nil) +if err != nil { + // Handle error. +} +fmt.Println(q.Human.Name) +fmt.Println(q.Human.Height) + +// Output: +// Luke Skywalker +// 1.72 +``` + +However, that'll only work if the arguments are constant and known in advance. Otherwise, you will need to make use of variables. Replace the constants in the struct field tag with variable names: + +```Go +var q struct { + Human struct { + Name graphql.String + Height graphql.Float `graphql:"height(unit: $unit)"` + } `graphql:"human(id: $id)"` +} +``` + +Then, define a `variables` map with their values: + +```Go +variables := map[string]any{ + "id": graphql.ID(id), + "unit": starwars.LengthUnit("METER"), +} +``` + +Finally, call `client.Query` providing `variables`: + +```Go +err := client.Query(context.Background(), &q, variables) +if err != nil { + // Handle error. +} +``` + +### Inline Fragments + +Some GraphQL queries contain inline fragments. You can use the `graphql` struct field tag to express them. + +For example, to make the following GraphQL query: + +```GraphQL +{ + hero(episode: "JEDI") { + name + ... on Droid { + primaryFunction + } + ... on Human { + height + } + } +} +``` + +You can define this variable: + +```Go +var q struct { + Hero struct { + Name graphql.String + Droid struct { + PrimaryFunction graphql.String + } `graphql:"... on Droid"` + Human struct { + Height graphql.Float + } `graphql:"... on Human"` + } `graphql:"hero(episode: \"JEDI\")"` +} +``` + +Alternatively, you can define the struct types corresponding to inline fragments, and use them as embedded fields in your query: + +```Go +type ( + DroidFragment struct { + PrimaryFunction graphql.String + } + HumanFragment struct { + Height graphql.Float + } +) + +var q struct { + Hero struct { + Name graphql.String + DroidFragment `graphql:"... on Droid"` + HumanFragment `graphql:"... on Human"` + } `graphql:"hero(episode: \"JEDI\")"` +} +``` + +Then call `client.Query`: + +```Go +err := client.Query(context.Background(), &q, nil) +if err != nil { + // Handle error. +} +fmt.Println(q.Hero.Name) +fmt.Println(q.Hero.PrimaryFunction) +fmt.Println(q.Hero.Height) + +// Output: +// R2-D2 +// Astromech +// 0 +``` + +### Mutations + +Mutations often require information that you can only find out by performing a query first. Let's suppose you've already done that. + +For example, to make the following GraphQL mutation: + +```GraphQL +mutation($ep: Episode!, $review: ReviewInput!) { + createReview(episode: $ep, review: $review) { + stars + commentary + } +} +variables { + "ep": "JEDI", + "review": { + "stars": 5, + "commentary": "This is a great movie!" + } +} +``` + +You can define: + +```Go +var m struct { + CreateReview struct { + Stars graphql.Int + Commentary graphql.String + } `graphql:"createReview(episode: $ep, review: $review)"` +} +variables := map[string]any{ + "ep": starwars.Episode("JEDI"), + "review": starwars.ReviewInput{ + Stars: graphql.Int(5), + Commentary: graphql.String("This is a great movie!"), + }, +} +``` + +Then call `client.Mutate`: + +```Go +err := client.Mutate(context.Background(), &m, variables) +if err != nil { + // Handle error. +} +fmt.Printf("Created a %v star review: %v\n", m.CreateReview.Stars, m.CreateReview.Commentary) + +// Output: +// Created a 5 star review: This is a great movie! +``` + +License +------- + +- [MIT License](LICENSE) diff --git a/vendor/github.com/cli/shurcooL-graphql/graphql.go b/vendor/github.com/cli/shurcooL-graphql/graphql.go new file mode 100644 index 000000000..d45cbbb58 --- /dev/null +++ b/vendor/github.com/cli/shurcooL-graphql/graphql.go @@ -0,0 +1,148 @@ +package graphql + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "github.com/cli/shurcooL-graphql/internal/jsonutil" +) + +// Client is a GraphQL client. +type Client struct { + url string // GraphQL server URL. + httpClient *http.Client // Non-nil. +} + +// NewClient creates a GraphQL client targeting the specified GraphQL server URL. +// If httpClient is nil, then http.DefaultClient is used. +func NewClient(url string, httpClient *http.Client) *Client { + if httpClient == nil { + httpClient = http.DefaultClient + } + return &Client{ + url: url, + httpClient: httpClient, + } +} + +// Query executes a single GraphQL query request, +// with a query derived from q, populating the response into it. +// Argument q should be a pointer to struct that corresponds to the GraphQL schema. +func (c *Client) Query(ctx context.Context, q any, variables map[string]any) error { + return c.do(ctx, queryOperation, q, variables, "") +} + +// QueryNamed is the same as Query but allows a name to be specified for the query. +func (c *Client) QueryNamed(ctx context.Context, queryName string, q any, variables map[string]any) error { + return c.do(ctx, queryOperation, q, variables, queryName) +} + +// Mutate executes a single GraphQL mutation request, +// with a mutation derived from m, populating the response into it. +// Argument m should be a pointer to struct that corresponds to the GraphQL schema. +func (c *Client) Mutate(ctx context.Context, m any, variables map[string]any) error { + return c.do(ctx, mutationOperation, m, variables, "") +} + +// MutateNamed is the same as Mutate but allows a name to be specified for the mutation. +func (c *Client) MutateNamed(ctx context.Context, queryName string, m any, variables map[string]any) error { + return c.do(ctx, mutationOperation, m, variables, queryName) +} + +// do executes a single GraphQL operation. +func (c *Client) do(ctx context.Context, op operationType, v any, variables map[string]any, queryName string) error { + var query string + switch op { + case queryOperation: + query = constructQuery(v, variables, queryName) + case mutationOperation: + query = constructMutation(v, variables, queryName) + } + in := struct { + Query string `json:"query"` + Variables map[string]any `json:"variables,omitempty"` + }{ + Query: query, + Variables: variables, + } + var buf bytes.Buffer + err := json.NewEncoder(&buf).Encode(in) + if err != nil { + return err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.url, &buf) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + resp, err := c.httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("non-200 OK status code: %v body: %q", resp.Status, body) + } + var out struct { + Data *json.RawMessage + Errors Errors + //Extensions any // Unused. + } + err = json.NewDecoder(resp.Body).Decode(&out) + if err != nil { + // TODO: Consider including response body in returned error, if deemed helpful. + return err + } + if out.Data != nil { + err := jsonutil.UnmarshalGraphQL(*out.Data, v) + if err != nil { + // TODO: Consider including response body in returned error, if deemed helpful. + return err + } + } + if len(out.Errors) > 0 { + return out.Errors + } + return nil +} + +// Errors represents the "errors" array in a response from a GraphQL server. +// If returned via error interface, the slice is expected to contain at least 1 element. +// +// Specification: https://spec.graphql.org/October2021/#sec-Errors. +type Errors []struct { + Message string + Locations []struct { + Line int + Column int + } + Path []any + Extensions map[string]any + Type string +} + +// Error implements error interface. +func (e Errors) Error() string { + b := strings.Builder{} + l := len(e) + for i, err := range e { + b.WriteString(fmt.Sprintf("Message: %s, Locations: %+v", err.Message, err.Locations)) + if i != l-1 { + b.WriteString("\n") + } + } + return b.String() +} + +type operationType uint8 + +const ( + queryOperation operationType = iota + mutationOperation +) diff --git a/vendor/github.com/cli/shurcooL-graphql/ident/ident.go b/vendor/github.com/cli/shurcooL-graphql/ident/ident.go new file mode 100644 index 000000000..9fa925888 --- /dev/null +++ b/vendor/github.com/cli/shurcooL-graphql/ident/ident.go @@ -0,0 +1,241 @@ +// Package ident provides functions for parsing and converting identifier names +// between various naming convention. It has support for MixedCaps, lowerCamelCase, +// and SCREAMING_SNAKE_CASE naming conventions. +package ident + +import ( + "strings" + "unicode" + "unicode/utf8" +) + +// ParseMixedCaps parses a MixedCaps identifier name. +// +// E.g., "ClientMutationID" -> {"Client", "Mutation", "ID"}. +func ParseMixedCaps(name string) Name { + var words Name + + // Split name at any lower -> Upper or Upper -> Upper,lower transitions. + // Check each word for initialisms. + runes := []rune(name) + w, i := 0, 0 // Index of start of word, scan. + for i+1 <= len(runes) { + eow := false // Whether we hit the end of a word. + if i+1 == len(runes) { + eow = true + } else if unicode.IsLower(runes[i]) && unicode.IsUpper(runes[i+1]) { + // lower -> Upper. + eow = true + } else if i+2 < len(runes) && unicode.IsUpper(runes[i]) && unicode.IsUpper(runes[i+1]) && unicode.IsLower(runes[i+2]) { + // Upper -> Upper,lower. End of acronym, followed by a word. + eow = true + + if string(runes[i:i+3]) == "IDs" { // Special case, plural form of ID initialism. + eow = false + } + } + i++ + if !eow { + continue + } + + // [w, i) is a word. + word := string(runes[w:i]) + if initialism, ok := isInitialism(word); ok { + words = append(words, initialism) + } else if i1, i2, ok := isTwoInitialisms(word); ok { + words = append(words, i1, i2) + } else { + words = append(words, word) + } + w = i + } + return words +} + +// ParseLowerCamelCase parses a lowerCamelCase identifier name. +// +// E.g., "clientMutationId" -> {"client", "Mutation", "Id"}. +func ParseLowerCamelCase(name string) Name { + var words Name + + // Split name at any Upper letters. + runes := []rune(name) + w, i := 0, 0 // Index of start of word, scan. + for i+1 <= len(runes) { + eow := false // Whether we hit the end of a word. + if i+1 == len(runes) { + eow = true + } else if unicode.IsUpper(runes[i+1]) { + // Upper letter. + eow = true + } + i++ + if !eow { + continue + } + + // [w, i) is a word. + words = append(words, string(runes[w:i])) + w = i + } + return words +} + +// ParseScreamingSnakeCase parses a SCREAMING_SNAKE_CASE identifier name. +// +// E.g., "CLIENT_MUTATION_ID" -> {"CLIENT", "MUTATION", "ID"}. +func ParseScreamingSnakeCase(name string) Name { + var words Name + + // Split name at '_' characters. + runes := []rune(name) + w, i := 0, 0 // Index of start of word, scan. + for i+1 <= len(runes) { + eow := false // Whether we hit the end of a word. + if i+1 == len(runes) { + eow = true + } else if runes[i+1] == '_' { + // Underscore. + eow = true + } + i++ + if !eow { + continue + } + + // [w, i) is a word. + words = append(words, string(runes[w:i])) + if i < len(runes) && runes[i] == '_' { + // Skip underscore. + i++ + } + w = i + } + return words +} + +// Name is an identifier name, broken up into individual words. +type Name []string + +// ToMixedCaps expresses identifier name in MixedCaps naming convention. +// +// E.g., "ClientMutationID". +func (n Name) ToMixedCaps() string { + for i, word := range n { + if strings.EqualFold(word, "IDs") { // Special case, plural form of ID initialism. + n[i] = "IDs" + continue + } + if initialism, ok := isInitialism(word); ok { + n[i] = initialism + continue + } + if brand, ok := isBrand(word); ok { + n[i] = brand + continue + } + r, size := utf8.DecodeRuneInString(word) + n[i] = string(unicode.ToUpper(r)) + strings.ToLower(word[size:]) + } + return strings.Join(n, "") +} + +// ToLowerCamelCase expresses identifier name in lowerCamelCase naming convention. +// +// E.g., "clientMutationId". +func (n Name) ToLowerCamelCase() string { + for i, word := range n { + if i == 0 { + n[i] = strings.ToLower(word) + continue + } + r, size := utf8.DecodeRuneInString(word) + n[i] = string(unicode.ToUpper(r)) + strings.ToLower(word[size:]) + } + return strings.Join(n, "") +} + +// isInitialism reports whether word is an initialism. +func isInitialism(word string) (string, bool) { + initialism := strings.ToUpper(word) + _, ok := initialisms[initialism] + return initialism, ok +} + +// isTwoInitialisms reports whether word is two initialisms. +func isTwoInitialisms(word string) (string, string, bool) { + word = strings.ToUpper(word) + for i := 2; i <= len(word)-2; i++ { // Shortest initialism is 2 characters long. + _, ok1 := initialisms[word[:i]] + _, ok2 := initialisms[word[i:]] + if ok1 && ok2 { + return word[:i], word[i:], true + } + } + return "", "", false +} + +// initialisms is the set of initialisms in the MixedCaps naming convention. +// Only add entries that are highly unlikely to be non-initialisms. +// For instance, "ID" is fine (Freudian code is rare), but "AND" is not. +var initialisms = map[string]struct{}{ + "ACL": {}, + "API": {}, + "ASCII": {}, + "CPU": {}, + "CSS": {}, + "DNS": {}, + "EOF": {}, + "GUID": {}, + "HTML": {}, + "HTTP": {}, + "HTTPS": {}, + "ID": {}, + "IP": {}, + "JSON": {}, + "LHS": {}, + "QPS": {}, + "RAM": {}, + "RHS": {}, + "RPC": {}, + "RSS": {}, + "SLA": {}, + "SMTP": {}, + "SQL": {}, + "SSH": {}, + "TCP": {}, + "TLS": {}, + "TTL": {}, + "UDP": {}, + "UI": {}, + "UID": {}, + "URI": {}, + "URL": {}, + "UTF8": {}, + "UUID": {}, + "VM": {}, + "XML": {}, + "XMPP": {}, + "XSRF": {}, + "XSS": {}, +} + +// isBrand reports whether word is a brand. +func isBrand(word string) (string, bool) { + brand, ok := brands[strings.ToLower(word)] + return brand, ok +} + +// brands is the map of brands in the MixedCaps naming convention; +// see https://dmitri.shuralyov.com/idiomatic-go#for-brands-or-words-with-more-than-1-capital-letter-lowercase-all-letters. +// Key is the lower case version of the brand, value is the canonical brand spelling. +// Only add entries that are highly unlikely to be non-brands. +var brands = map[string]string{ + "github": "GitHub", + "gitlab": "GitLab", + "devops": "DevOps", // For https://en.wikipedia.org/wiki/DevOps. + // For https://docs.github.com/en/graphql/reference/enums#fundingplatform. + "issuehunt": "IssueHunt", + "lfx": "LFX", +} diff --git a/vendor/github.com/cli/shurcooL-graphql/internal/jsonutil/graphql.go b/vendor/github.com/cli/shurcooL-graphql/internal/jsonutil/graphql.go new file mode 100644 index 000000000..5a44ea593 --- /dev/null +++ b/vendor/github.com/cli/shurcooL-graphql/internal/jsonutil/graphql.go @@ -0,0 +1,310 @@ +// Package jsonutil provides a function for decoding JSON +// into a GraphQL query data structure. +package jsonutil + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "reflect" + "strings" +) + +// UnmarshalGraphQL parses the JSON-encoded GraphQL response data and stores +// the result in the GraphQL query data structure pointed to by v. +// +// The implementation is created on top of the JSON tokenizer available +// in "encoding/json".Decoder. +func UnmarshalGraphQL(data []byte, v any) error { + dec := json.NewDecoder(bytes.NewReader(data)) + dec.UseNumber() + err := (&decoder{tokenizer: dec}).Decode(v) + if err != nil { + return err + } + tok, err := dec.Token() + switch err { + case io.EOF: + // Expect to get io.EOF. There shouldn't be any more + // tokens left after we've decoded v successfully. + return nil + case nil: + return fmt.Errorf("invalid token '%v' after top-level value", tok) + default: + return err + } +} + +// decoder is a JSON decoder that performs custom unmarshaling behavior +// for GraphQL query data structures. It's implemented on top of a JSON tokenizer. +type decoder struct { + tokenizer interface { + Token() (json.Token, error) + } + + // Stack of what part of input JSON we're in the middle of - objects, arrays. + parseState []json.Delim + + // Stacks of values where to unmarshal. + // The top of each stack is the reflect.Value where to unmarshal next JSON value. + // + // The reason there's more than one stack is because we might be unmarshaling + // a single JSON value into multiple GraphQL fragments or embedded structs, so + // we keep track of them all. + vs [][]reflect.Value +} + +// Decode decodes a single JSON value from d.tokenizer into v. +func (d *decoder) Decode(v any) error { + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Ptr { + return fmt.Errorf("cannot decode into non-pointer %T", v) + } + d.vs = [][]reflect.Value{{rv.Elem()}} + return d.decode() +} + +// decode decodes a single JSON value from d.tokenizer into d.vs. +func (d *decoder) decode() error { + // The loop invariant is that the top of each d.vs stack + // is where we try to unmarshal the next JSON value we see. + for len(d.vs) > 0 { + tok, err := d.tokenizer.Token() + if err == io.EOF { + return errors.New("unexpected end of JSON input") + } else if err != nil { + return err + } + + switch { + + // Are we inside an object and seeing next key (rather than end of object)? + case d.state() == '{' && tok != json.Delim('}'): + key, ok := tok.(string) + if !ok { + return errors.New("unexpected non-key in JSON input") + } + someFieldExist := false + for i := range d.vs { + v := d.vs[i][len(d.vs[i])-1] + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + var f reflect.Value + if v.Kind() == reflect.Struct { + f = fieldByGraphQLName(v, key) + if f.IsValid() { + someFieldExist = true + } + } + d.vs[i] = append(d.vs[i], f) + } + if !someFieldExist { + return fmt.Errorf("struct field for %q doesn't exist in any of %v places to unmarshal", key, len(d.vs)) + } + + // We've just consumed the current token, which was the key. + // Read the next token, which should be the value, and let the rest of code process it. + tok, err = d.tokenizer.Token() + if err == io.EOF { + return errors.New("unexpected end of JSON input") + } else if err != nil { + return err + } + + // Are we inside an array and seeing next value (rather than end of array)? + case d.state() == '[' && tok != json.Delim(']'): + someSliceExist := false + for i := range d.vs { + v := d.vs[i][len(d.vs[i])-1] + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + var f reflect.Value + if v.Kind() == reflect.Slice { + v.Set(reflect.Append(v, reflect.Zero(v.Type().Elem()))) // v = append(v, T). + f = v.Index(v.Len() - 1) + someSliceExist = true + } + d.vs[i] = append(d.vs[i], f) + } + if !someSliceExist { + return fmt.Errorf("slice doesn't exist in any of %v places to unmarshal", len(d.vs)) + } + } + + switch tok := tok.(type) { + case string, json.Number, bool, nil: + // Value. + + for i := range d.vs { + v := d.vs[i][len(d.vs[i])-1] + if !v.IsValid() { + continue + } + err := unmarshalValue(tok, v) + if err != nil { + return err + } + } + d.popAllVs() + + case json.Delim: + switch tok { + case '{': + // Start of object. + + d.pushState(tok) + + frontier := make([]reflect.Value, len(d.vs)) // Places to look for GraphQL fragments/embedded structs. + for i := range d.vs { + v := d.vs[i][len(d.vs[i])-1] + frontier[i] = v + // TODO: Do this recursively or not? Add a test case if needed. + if v.Kind() == reflect.Ptr && v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) // v = new(T). + } + } + // Find GraphQL fragments/embedded structs recursively, adding to frontier + // as new ones are discovered and exploring them further. + for len(frontier) > 0 { + v := frontier[0] + frontier = frontier[1:] + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + if v.Kind() != reflect.Struct { + continue + } + for i := 0; i < v.NumField(); i++ { + if isGraphQLFragment(v.Type().Field(i)) || v.Type().Field(i).Anonymous { + // Add GraphQL fragment or embedded struct. + d.vs = append(d.vs, []reflect.Value{v.Field(i)}) + frontier = append(frontier, v.Field(i)) + } + } + } + case '[': + // Start of array. + + d.pushState(tok) + + for i := range d.vs { + v := d.vs[i][len(d.vs[i])-1] + // TODO: Confirm this is needed, write a test case. + //if v.Kind() == reflect.Ptr && v.IsNil() { + // v.Set(reflect.New(v.Type().Elem())) // v = new(T). + //} + + // Reset slice to empty (in case it had non-zero initial value). + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + if v.Kind() != reflect.Slice { + continue + } + v.Set(reflect.MakeSlice(v.Type(), 0, 0)) // v = make(T, 0, 0). + } + case '}', ']': + // End of object or array. + d.popAllVs() + d.popState() + default: + return errors.New("unexpected delimiter in JSON input") + } + default: + return errors.New("unexpected token in JSON input") + } + } + return nil +} + +// pushState pushes a new parse state s onto the stack. +func (d *decoder) pushState(s json.Delim) { + d.parseState = append(d.parseState, s) +} + +// popState pops a parse state (already obtained) off the stack. +// The stack must be non-empty. +func (d *decoder) popState() { + d.parseState = d.parseState[:len(d.parseState)-1] +} + +// state reports the parse state on top of stack, or 0 if empty. +func (d *decoder) state() json.Delim { + if len(d.parseState) == 0 { + return 0 + } + return d.parseState[len(d.parseState)-1] +} + +// popAllVs pops from all d.vs stacks, keeping only non-empty ones. +func (d *decoder) popAllVs() { + var nonEmpty [][]reflect.Value + for i := range d.vs { + d.vs[i] = d.vs[i][:len(d.vs[i])-1] + if len(d.vs[i]) > 0 { + nonEmpty = append(nonEmpty, d.vs[i]) + } + } + d.vs = nonEmpty +} + +// fieldByGraphQLName returns an exported struct field of struct v +// that matches GraphQL name, or invalid reflect.Value if none found. +func fieldByGraphQLName(v reflect.Value, name string) reflect.Value { + for i := 0; i < v.NumField(); i++ { + if v.Type().Field(i).PkgPath != "" { + // Skip unexported field. + continue + } + if hasGraphQLName(v.Type().Field(i), name) { + return v.Field(i) + } + } + return reflect.Value{} +} + +// hasGraphQLName reports whether struct field f has GraphQL name. +func hasGraphQLName(f reflect.StructField, name string) bool { + value, ok := f.Tag.Lookup("graphql") + if !ok { + // TODO: caseconv package is relatively slow. Optimize it, then consider using it here. + //return caseconv.MixedCapsToLowerCamelCase(f.Name) == name + return strings.EqualFold(f.Name, name) + } + value = strings.TrimSpace(value) // TODO: Parse better. + if strings.HasPrefix(value, "...") { + // GraphQL fragment. It doesn't have a name. + return false + } + // Cut off anything that follows the field name, + // such as field arguments, aliases, directives. + if i := strings.IndexAny(value, "(:@"); i != -1 { + value = value[:i] + } + return strings.TrimSpace(value) == name +} + +// isGraphQLFragment reports whether struct field f is a GraphQL fragment. +func isGraphQLFragment(f reflect.StructField) bool { + value, ok := f.Tag.Lookup("graphql") + if !ok { + return false + } + value = strings.TrimSpace(value) // TODO: Parse better. + return strings.HasPrefix(value, "...") +} + +// unmarshalValue unmarshals JSON value into v. +// Argument v must be addressable and not obtained by the use of unexported +// struct fields, otherwise unmarshalValue will panic. +func unmarshalValue(value json.Token, v reflect.Value) error { + b, err := json.Marshal(value) // TODO: Short-circuit (if profiling says it's worth it). + if err != nil { + return err + } + return json.Unmarshal(b, v.Addr().Interface()) +} diff --git a/vendor/github.com/cli/shurcooL-graphql/query.go b/vendor/github.com/cli/shurcooL-graphql/query.go new file mode 100644 index 000000000..16eaecd2e --- /dev/null +++ b/vendor/github.com/cli/shurcooL-graphql/query.go @@ -0,0 +1,140 @@ +package graphql + +import ( + "bytes" + "encoding/json" + "io" + "reflect" + "sort" + + "github.com/cli/shurcooL-graphql/ident" +) + +func constructQuery(v any, variables map[string]any, queryName string) string { + query := query(v) + if len(variables) > 0 { + return "query" + queryNameFormat(queryName) + "(" + queryArguments(variables) + ")" + query + } else if queryName != "" { + return "query" + queryNameFormat(queryName) + query + } + return query +} + +func constructMutation(v any, variables map[string]any, queryName string) string { + query := query(v) + if len(variables) > 0 { + return "mutation" + queryNameFormat(queryName) + "(" + queryArguments(variables) + ")" + query + } + return "mutation" + queryNameFormat(queryName) + query +} + +func queryNameFormat(n string) string { + if n != "" { + return " " + n + } + return n +} + +// queryArguments constructs a minified arguments string for variables. +// +// E.g., map[string]any{"a": Int(123), "b": NewBoolean(true)} -> "$a:Int!$b:Boolean". +func queryArguments(variables map[string]any) string { + // Sort keys in order to produce deterministic output for testing purposes. + // TODO: If tests can be made to work with non-deterministic output, then no need to sort. + keys := make([]string, 0, len(variables)) + for k := range variables { + keys = append(keys, k) + } + sort.Strings(keys) + + var buf bytes.Buffer + for _, k := range keys { + _, _ = io.WriteString(&buf, "$") + _, _ = io.WriteString(&buf, k) + _, _ = io.WriteString(&buf, ":") + writeArgumentType(&buf, reflect.TypeOf(variables[k]), true) + // Don't insert a comma here. + // Commas in GraphQL are insignificant, and we want minified output. + // See https://spec.graphql.org/October2021/#sec-Insignificant-Commas. + } + return buf.String() +} + +// writeArgumentType writes a minified GraphQL type for t to w. +// Argument value indicates whether t is a value (required) type or pointer (optional) type. +// If value is true, then "!" is written at the end of t. +func writeArgumentType(w io.Writer, t reflect.Type, value bool) { + if t.Kind() == reflect.Ptr { + // Pointer is an optional type, so no "!" at the end of the pointer's underlying type. + writeArgumentType(w, t.Elem(), false) + return + } + + switch t.Kind() { + case reflect.Slice, reflect.Array: + // List. E.g., "[Int]". + _, _ = io.WriteString(w, "[") + writeArgumentType(w, t.Elem(), true) + _, _ = io.WriteString(w, "]") + default: + // Named type. E.g., "Int". + name := t.Name() + if name == "string" { // HACK: Workaround for https://github.com/shurcooL/githubv4/issues/12. + name = "ID" + } + _, _ = io.WriteString(w, name) + } + + if value { + // Value is a required type, so add "!" to the end. + _, _ = io.WriteString(w, "!") + } +} + +// query uses writeQuery to recursively construct +// a minified query string from the provided struct v. +// +// E.g., struct{Foo Int, BarBaz *Boolean} -> "{foo,barBaz}". +func query(v any) string { + var buf bytes.Buffer + writeQuery(&buf, reflect.TypeOf(v), false) + return buf.String() +} + +// writeQuery writes a minified query for t to w. +// If inline is true, the struct fields of t are inlined into parent struct. +func writeQuery(w io.Writer, t reflect.Type, inline bool) { + switch t.Kind() { + case reflect.Ptr, reflect.Slice: + writeQuery(w, t.Elem(), false) + case reflect.Struct: + // If the type implements json.Unmarshaler, it's a scalar. Don't expand it. + if reflect.PtrTo(t).Implements(jsonUnmarshaler) { + return + } + if !inline { + _, _ = io.WriteString(w, "{") + } + for i := 0; i < t.NumField(); i++ { + if i != 0 { + _, _ = io.WriteString(w, ",") + } + f := t.Field(i) + value, ok := f.Tag.Lookup("graphql") + inlineField := f.Anonymous && !ok + if !inlineField { + if ok { + _, _ = io.WriteString(w, value) + } else { + _, _ = io.WriteString(w, ident.ParseMixedCaps(f.Name).ToLowerCamelCase()) + } + } + writeQuery(w, f.Type, inlineField) + } + if !inline { + _, _ = io.WriteString(w, "}") + } + } +} + +var jsonUnmarshaler = reflect.TypeOf((*json.Unmarshaler)(nil)).Elem() diff --git a/vendor/github.com/cli/shurcooL-graphql/scalar.go b/vendor/github.com/cli/shurcooL-graphql/scalar.go new file mode 100644 index 000000000..8679d34e2 --- /dev/null +++ b/vendor/github.com/cli/shurcooL-graphql/scalar.go @@ -0,0 +1,51 @@ +package graphql + +// Note: These custom types are meant to be used in queries for now. +// But the plan is to switch to using native Go types (string, int, bool, time.Time, etc.). +// See https://github.com/shurcooL/githubv4/issues/9 for details. +// +// These custom types currently provide documentation, and their use +// is required for sending outbound queries. However, native Go types +// can be used for unmarshaling. Once https://github.com/shurcooL/githubv4/issues/9 +// is resolved, native Go types can completely replace these. + +type ( + // Boolean represents true or false values. + Boolean bool + + // Float represents signed double-precision fractional values as + // specified by IEEE 754. + Float float64 + + // ID represents a unique identifier that is Base64 obfuscated. It + // is often used to refetch an object or as key for a cache. The ID + // type appears in a JSON response as a String; however, it is not + // intended to be human-readable. When expected as an input type, + // any string (such as "VXNlci0xMA==") or integer (such as 4) input + // value will be accepted as an ID. + ID any + + // Int represents non-fractional signed whole numeric values. + // Int can represent values between -(2^31) and 2^31 - 1. + Int int32 + + // String represents textual data as UTF-8 character sequences. + // This type is most often used by GraphQL to represent free-form + // human-readable text. + String string +) + +// NewBoolean is a helper to make a new *Boolean. +func NewBoolean(v Boolean) *Boolean { return &v } + +// NewFloat is a helper to make a new *Float. +func NewFloat(v Float) *Float { return &v } + +// NewID is a helper to make a new *ID. +func NewID(v ID) *ID { return &v } + +// NewInt is a helper to make a new *Int. +func NewInt(v Int) *Int { return &v } + +// NewString is a helper to make a new *String. +func NewString(v String) *String { return &v } diff --git a/vendor/github.com/danieljoos/wincred/.gitattributes b/vendor/github.com/danieljoos/wincred/.gitattributes new file mode 100644 index 000000000..d207b1802 --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/.gitattributes @@ -0,0 +1 @@ +*.go text eol=lf diff --git a/vendor/github.com/danieljoos/wincred/.gitignore b/vendor/github.com/danieljoos/wincred/.gitignore new file mode 100644 index 000000000..6142c0691 --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/.gitignore @@ -0,0 +1,25 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test + +coverage.txt diff --git a/vendor/github.com/danieljoos/wincred/LICENSE b/vendor/github.com/danieljoos/wincred/LICENSE new file mode 100644 index 000000000..2f436f1b3 --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Daniel Joos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/danieljoos/wincred/README.md b/vendor/github.com/danieljoos/wincred/README.md new file mode 100644 index 000000000..8a879b0ce --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/README.md @@ -0,0 +1,145 @@ +wincred +======= + +Go wrapper around the Windows Credential Manager API functions. + +[![GitHub release](https://img.shields.io/github/release/danieljoos/wincred.svg?style=flat-square)](https://github.com/danieljoos/wincred/releases/latest) +[![Test Status](https://img.shields.io/github/actions/workflow/status/danieljoos/wincred/test.yml?label=test&logo=github&style=flat-square)](https://github.com/danieljoos/wincred/actions?query=workflow%3Atest) +[![Go Report Card](https://goreportcard.com/badge/github.com/danieljoos/wincred)](https://goreportcard.com/report/github.com/danieljoos/wincred) +[![Codecov](https://img.shields.io/codecov/c/github/danieljoos/wincred?logo=codecov&style=flat-square)](https://codecov.io/gh/danieljoos/wincred) +[![PkgGoDev](https://img.shields.io/badge/go.dev-docs-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/danieljoos/wincred) + +Installation +------------ + +```Go +go get github.com/danieljoos/wincred +``` + + +Usage +----- + +See the following examples: + +### Create and store a new generic credential object +```Go +package main + +import ( + "fmt" + "github.com/danieljoos/wincred" +) + +func main() { + cred := wincred.NewGenericCredential("myGoApplication") + cred.CredentialBlob = []byte("my secret") + err := cred.Write() + + if err != nil { + fmt.Println(err) + } +} +``` + +### Retrieve a credential object +```Go +package main + +import ( + "fmt" + "github.com/danieljoos/wincred" +) + +func main() { + cred, err := wincred.GetGenericCredential("myGoApplication") + if err == nil { + fmt.Println(string(cred.CredentialBlob)) + } +} +``` + +### Remove a credential object +```Go +package main + +import ( + "fmt" + "github.com/danieljoos/wincred" +) + +func main() { + cred, err := wincred.GetGenericCredential("myGoApplication") + if err != nil { + fmt.Println(err) + return + } + cred.Delete() +} +``` + +### List all available credentials +```Go +package main + +import ( + "fmt" + "github.com/danieljoos/wincred" +) + +func main() { + creds, err := wincred.List() + if err != nil { + fmt.Println(err) + return + } + for i := range(creds) { + fmt.Println(creds[i].TargetName) + } +} +``` + +Hints +----- + +### Encoding + +The credential objects simply store byte arrays without specific meaning or encoding. +For sharing between different applications, it might make sense to apply an explicit string encoding - for example **UTF-16 LE** (used nearly everywhere in the Win32 API). + +```Go +package main + +import ( + "fmt" + "os" + + "github.com/danieljoos/wincred" + "golang.org/x/text/encoding/unicode" + "golang.org/x/text/transform" +) + +func main() { + cred := wincred.NewGenericCredential("myGoApplication") + + encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder() + blob, _, err := transform.Bytes(encoder, []byte("mysecret")) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + cred.CredentialBlob = blob + err = cred.Write() + + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +``` + +### Limitations + +The size of a credential blob is limited to **2560 Bytes** by the Windows API. diff --git a/vendor/github.com/danieljoos/wincred/conversion.go b/vendor/github.com/danieljoos/wincred/conversion.go new file mode 100644 index 000000000..bc04f50f1 --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/conversion.go @@ -0,0 +1,116 @@ +// +build windows + +package wincred + +import ( + "encoding/binary" + "reflect" + "time" + "unsafe" + + syscall "golang.org/x/sys/windows" +) + +// utf16ToByte creates a byte array from a given UTF 16 char array. +func utf16ToByte(wstr []uint16) (result []byte) { + result = make([]byte, len(wstr)*2) + for i := range wstr { + binary.LittleEndian.PutUint16(result[(i*2):(i*2)+2], wstr[i]) + } + return +} + +// utf16FromString creates a UTF16 char array from a string. +func utf16FromString(str string) []uint16 { + res, err := syscall.UTF16FromString(str) + if err != nil { + return []uint16{} + } + return res +} + +// goBytes copies the given C byte array to a Go byte array (see `C.GoBytes`). +// This function avoids having cgo as dependency. +func goBytes(src uintptr, len uint32) []byte { + if src == uintptr(0) { + return []byte{} + } + rv := make([]byte, len) + copy(rv, *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ + Data: src, + Len: int(len), + Cap: int(len), + }))) + return rv +} + +// Convert the given CREDENTIAL struct to a more usable structure +func sysToCredential(cred *sysCREDENTIAL) (result *Credential) { + if cred == nil { + return nil + } + result = new(Credential) + result.Comment = syscall.UTF16PtrToString(cred.Comment) + result.TargetName = syscall.UTF16PtrToString(cred.TargetName) + result.TargetAlias = syscall.UTF16PtrToString(cred.TargetAlias) + result.UserName = syscall.UTF16PtrToString(cred.UserName) + result.LastWritten = time.Unix(0, cred.LastWritten.Nanoseconds()) + result.Persist = CredentialPersistence(cred.Persist) + result.CredentialBlob = goBytes(cred.CredentialBlob, cred.CredentialBlobSize) + result.Attributes = make([]CredentialAttribute, cred.AttributeCount) + attrSlice := *(*[]sysCREDENTIAL_ATTRIBUTE)(unsafe.Pointer(&reflect.SliceHeader{ + Data: cred.Attributes, + Len: int(cred.AttributeCount), + Cap: int(cred.AttributeCount), + })) + for i, attr := range attrSlice { + resultAttr := &result.Attributes[i] + resultAttr.Keyword = syscall.UTF16PtrToString(attr.Keyword) + resultAttr.Value = goBytes(attr.Value, attr.ValueSize) + } + return result +} + +// Convert the given Credential object back to a CREDENTIAL struct, which can be used for calling the +// Windows APIs +func sysFromCredential(cred *Credential) (result *sysCREDENTIAL) { + if cred == nil { + return nil + } + result = new(sysCREDENTIAL) + result.Flags = 0 + result.Type = 0 + result.TargetName, _ = syscall.UTF16PtrFromString(cred.TargetName) + result.Comment, _ = syscall.UTF16PtrFromString(cred.Comment) + result.LastWritten = syscall.NsecToFiletime(cred.LastWritten.UnixNano()) + result.CredentialBlobSize = uint32(len(cred.CredentialBlob)) + if len(cred.CredentialBlob) > 0 { + result.CredentialBlob = uintptr(unsafe.Pointer(&cred.CredentialBlob[0])) + } else { + result.CredentialBlob = 0 + } + result.Persist = uint32(cred.Persist) + result.AttributeCount = uint32(len(cred.Attributes)) + attributes := make([]sysCREDENTIAL_ATTRIBUTE, len(cred.Attributes)) + if len(attributes) > 0 { + result.Attributes = uintptr(unsafe.Pointer(&attributes[0])) + } else { + result.Attributes = 0 + } + for i := range cred.Attributes { + inAttr := &cred.Attributes[i] + outAttr := &attributes[i] + outAttr.Keyword, _ = syscall.UTF16PtrFromString(inAttr.Keyword) + outAttr.Flags = 0 + outAttr.ValueSize = uint32(len(inAttr.Value)) + if len(inAttr.Value) > 0 { + outAttr.Value = uintptr(unsafe.Pointer(&inAttr.Value[0])) + } else { + outAttr.Value = 0 + } + } + result.TargetAlias, _ = syscall.UTF16PtrFromString(cred.TargetAlias) + result.UserName, _ = syscall.UTF16PtrFromString(cred.UserName) + + return +} diff --git a/vendor/github.com/danieljoos/wincred/conversion_unsupported.go b/vendor/github.com/danieljoos/wincred/conversion_unsupported.go new file mode 100644 index 000000000..a1ea72075 --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/conversion_unsupported.go @@ -0,0 +1,11 @@ +// +build !windows + +package wincred + +func utf16ToByte(...interface{}) []byte { + return nil +} + +func utf16FromString(...interface{}) []uint16 { + return nil +} diff --git a/vendor/github.com/danieljoos/wincred/sys.go b/vendor/github.com/danieljoos/wincred/sys.go new file mode 100644 index 000000000..fb8a6ac0f --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/sys.go @@ -0,0 +1,148 @@ +//go:build windows +// +build windows + +package wincred + +import ( + "reflect" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") + procCredRead = modadvapi32.NewProc("CredReadW") + procCredWrite proc = modadvapi32.NewProc("CredWriteW") + procCredDelete proc = modadvapi32.NewProc("CredDeleteW") + procCredFree proc = modadvapi32.NewProc("CredFree") + procCredEnumerate = modadvapi32.NewProc("CredEnumerateW") +) + +// Interface for syscall.Proc: helps testing +type proc interface { + Call(a ...uintptr) (r1, r2 uintptr, lastErr error) +} + +// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_credentialw +type sysCREDENTIAL struct { + Flags uint32 + Type uint32 + TargetName *uint16 + Comment *uint16 + LastWritten windows.Filetime + CredentialBlobSize uint32 + CredentialBlob uintptr + Persist uint32 + AttributeCount uint32 + Attributes uintptr + TargetAlias *uint16 + UserName *uint16 +} + +// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_credential_attributew +type sysCREDENTIAL_ATTRIBUTE struct { + Keyword *uint16 + Flags uint32 + ValueSize uint32 + Value uintptr +} + +// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_credentialw +type sysCRED_TYPE uint32 + +const ( + sysCRED_TYPE_GENERIC sysCRED_TYPE = 0x1 + sysCRED_TYPE_DOMAIN_PASSWORD sysCRED_TYPE = 0x2 + sysCRED_TYPE_DOMAIN_CERTIFICATE sysCRED_TYPE = 0x3 + sysCRED_TYPE_DOMAIN_VISIBLE_PASSWORD sysCRED_TYPE = 0x4 + sysCRED_TYPE_GENERIC_CERTIFICATE sysCRED_TYPE = 0x5 + sysCRED_TYPE_DOMAIN_EXTENDED sysCRED_TYPE = 0x6 + + // https://docs.microsoft.com/en-us/windows/desktop/Debug/system-error-codes + sysERROR_NOT_FOUND = windows.Errno(1168) + sysERROR_INVALID_PARAMETER = windows.Errno(87) + sysERROR_BAD_USERNAME = windows.Errno(2202) +) + +// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-credreadw +func sysCredRead(targetName string, typ sysCRED_TYPE) (*Credential, error) { + var pcred *sysCREDENTIAL + targetNamePtr, _ := windows.UTF16PtrFromString(targetName) + ret, _, err := syscall.SyscallN( + procCredRead.Addr(), + uintptr(unsafe.Pointer(targetNamePtr)), + uintptr(typ), + 0, + uintptr(unsafe.Pointer(&pcred)), + ) + if ret == 0 { + return nil, err + } + defer procCredFree.Call(uintptr(unsafe.Pointer(pcred))) + + return sysToCredential(pcred), nil +} + +// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-credwritew +func sysCredWrite(cred *Credential, typ sysCRED_TYPE) error { + ncred := sysFromCredential(cred) + ncred.Type = uint32(typ) + ret, _, err := procCredWrite.Call( + uintptr(unsafe.Pointer(ncred)), + 0, + ) + if ret == 0 { + return err + } + + return nil +} + +// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-creddeletew +func sysCredDelete(cred *Credential, typ sysCRED_TYPE) error { + targetNamePtr, _ := windows.UTF16PtrFromString(cred.TargetName) + ret, _, err := procCredDelete.Call( + uintptr(unsafe.Pointer(targetNamePtr)), + uintptr(typ), + 0, + ) + if ret == 0 { + return err + } + + return nil +} + +// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-credenumeratew +func sysCredEnumerate(filter string, all bool) ([]*Credential, error) { + var count int + var pcreds uintptr + var filterPtr *uint16 + if !all { + filterPtr, _ = windows.UTF16PtrFromString(filter) + } + ret, _, err := syscall.SyscallN( + procCredEnumerate.Addr(), + uintptr(unsafe.Pointer(filterPtr)), + 0, + uintptr(unsafe.Pointer(&count)), + uintptr(unsafe.Pointer(&pcreds)), + ) + if ret == 0 { + return nil, err + } + defer procCredFree.Call(pcreds) + credsSlice := *(*[]*sysCREDENTIAL)(unsafe.Pointer(&reflect.SliceHeader{ + Data: pcreds, + Len: count, + Cap: count, + })) + creds := make([]*Credential, count, count) + for i, cred := range credsSlice { + creds[i] = sysToCredential(cred) + } + + return creds, nil +} diff --git a/vendor/github.com/danieljoos/wincred/sys_unsupported.go b/vendor/github.com/danieljoos/wincred/sys_unsupported.go new file mode 100644 index 000000000..b47bccf8a --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/sys_unsupported.go @@ -0,0 +1,36 @@ +// +build !windows + +package wincred + +import ( + "errors" + "syscall" +) + +const ( + sysCRED_TYPE_GENERIC = 0 + sysCRED_TYPE_DOMAIN_PASSWORD = 0 + sysCRED_TYPE_DOMAIN_CERTIFICATE = 0 + sysCRED_TYPE_DOMAIN_VISIBLE_PASSWORD = 0 + sysCRED_TYPE_GENERIC_CERTIFICATE = 0 + sysCRED_TYPE_DOMAIN_EXTENDED = 0 + + sysERROR_NOT_FOUND = syscall.Errno(1) + sysERROR_INVALID_PARAMETER = syscall.Errno(1) +) + +func sysCredRead(...interface{}) (*Credential, error) { + return nil, errors.New("Operation not supported") +} + +func sysCredWrite(...interface{}) error { + return errors.New("Operation not supported") +} + +func sysCredDelete(...interface{}) error { + return errors.New("Operation not supported") +} + +func sysCredEnumerate(...interface{}) ([]*Credential, error) { + return nil, errors.New("Operation not supported") +} diff --git a/vendor/github.com/danieljoos/wincred/types.go b/vendor/github.com/danieljoos/wincred/types.go new file mode 100644 index 000000000..28debc932 --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/types.go @@ -0,0 +1,69 @@ +package wincred + +import ( + "time" +) + +// CredentialPersistence describes one of three persistence modes of a credential. +// A detailed description of the available modes can be found on +// Docs: https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_credentialw +type CredentialPersistence uint32 + +const ( + // PersistSession indicates that the credential only persists for the life + // of the current Windows login session. Such a credential is not visible in + // any other logon session, even from the same user. + PersistSession CredentialPersistence = 0x1 + + // PersistLocalMachine indicates that the credential persists for this and + // all subsequent logon sessions on this local machine/computer. It is + // however not visible for logon sessions of this user on a different + // machine. + PersistLocalMachine CredentialPersistence = 0x2 + + // PersistEnterprise indicates that the credential persists for this and all + // subsequent logon sessions for this user. It is also visible for logon + // sessions on different computers. + PersistEnterprise CredentialPersistence = 0x3 +) + +// CredentialAttribute represents an application-specific attribute of a credential. +type CredentialAttribute struct { + Keyword string + Value []byte +} + +// Credential is the basic credential structure. +// A credential is identified by its target name. +// The actual credential secret is available in the CredentialBlob field. +type Credential struct { + TargetName string + Comment string + LastWritten time.Time + CredentialBlob []byte + Attributes []CredentialAttribute + TargetAlias string + UserName string + Persist CredentialPersistence +} + +// GenericCredential holds a credential for generic usage. +// It is typically defined and used by applications that need to manage user +// secrets. +// +// More information about the available kinds of credentials of the Windows +// Credential Management API can be found on Docs: +// https://docs.microsoft.com/en-us/windows/desktop/SecAuthN/kinds-of-credentials +type GenericCredential struct { + Credential +} + +// DomainPassword holds a domain credential that is typically used by the +// operating system for user logon. +// +// More information about the available kinds of credentials of the Windows +// Credential Management API can be found on Docs: +// https://docs.microsoft.com/en-us/windows/desktop/SecAuthN/kinds-of-credentials +type DomainPassword struct { + Credential +} diff --git a/vendor/github.com/danieljoos/wincred/wincred.go b/vendor/github.com/danieljoos/wincred/wincred.go new file mode 100644 index 000000000..5632ee90c --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/wincred.go @@ -0,0 +1,114 @@ +// Package wincred provides primitives for accessing the Windows Credentials Management API. +// This includes functions for retrieval, listing and storage of credentials as well as Go structures for convenient access to the credential data. +// +// A more detailed description of Windows Credentials Management can be found on +// Docs: https://docs.microsoft.com/en-us/windows/desktop/SecAuthN/credentials-management +package wincred + +import "errors" + +const ( + // ErrElementNotFound is the error that is returned if a requested element cannot be found. + // This error constant can be used to check if a credential could not be found. + ErrElementNotFound = sysERROR_NOT_FOUND + + // ErrInvalidParameter is the error that is returned for invalid parameters. + // This error constant can be used to check if the given function parameters were invalid. + // For example when trying to create a new generic credential with an empty target name. + ErrInvalidParameter = sysERROR_INVALID_PARAMETER + + // ErrBadUsername is returned when the credential's username is invalid. + ErrBadUsername = sysERROR_BAD_USERNAME +) + +// GetGenericCredential fetches the generic credential with the given name from Windows credential manager. +// It returns nil and an error if the credential could not be found or an error occurred. +func GetGenericCredential(targetName string) (*GenericCredential, error) { + cred, err := sysCredRead(targetName, sysCRED_TYPE_GENERIC) + if cred != nil { + return &GenericCredential{Credential: *cred}, err + } + return nil, err +} + +// NewGenericCredential creates a new generic credential object with the given name. +// The persist mode of the newly created object is set to a default value that indicates local-machine-wide storage. +// The credential object is NOT yet persisted to the Windows credential vault. +func NewGenericCredential(targetName string) (result *GenericCredential) { + result = new(GenericCredential) + result.TargetName = targetName + result.Persist = PersistLocalMachine + return +} + +// Write persists the generic credential object to Windows credential manager. +func (t *GenericCredential) Write() (err error) { + err = sysCredWrite(&t.Credential, sysCRED_TYPE_GENERIC) + return +} + +// Delete removes the credential object from Windows credential manager. +func (t *GenericCredential) Delete() (err error) { + err = sysCredDelete(&t.Credential, sysCRED_TYPE_GENERIC) + return +} + +// GetDomainPassword fetches the domain-password credential with the given target host name from Windows credential manager. +// It returns nil and an error if the credential could not be found or an error occurred. +func GetDomainPassword(targetName string) (*DomainPassword, error) { + cred, err := sysCredRead(targetName, sysCRED_TYPE_DOMAIN_PASSWORD) + if cred != nil { + return &DomainPassword{Credential: *cred}, err + } + return nil, err +} + +// NewDomainPassword creates a new domain-password credential used for login to the given target host name. +// The persist mode of the newly created object is set to a default value that indicates local-machine-wide storage. +// The credential object is NOT yet persisted to the Windows credential vault. +func NewDomainPassword(targetName string) (result *DomainPassword) { + result = new(DomainPassword) + result.TargetName = targetName + result.Persist = PersistLocalMachine + return +} + +// Write persists the domain-password credential to Windows credential manager. +func (t *DomainPassword) Write() (err error) { + err = sysCredWrite(&t.Credential, sysCRED_TYPE_DOMAIN_PASSWORD) + return +} + +// Delete removes the domain-password credential from Windows credential manager. +func (t *DomainPassword) Delete() (err error) { + err = sysCredDelete(&t.Credential, sysCRED_TYPE_DOMAIN_PASSWORD) + return +} + +// SetPassword sets the CredentialBlob field of a domain password credential to the given string. +func (t *DomainPassword) SetPassword(pw string) { + t.CredentialBlob = utf16ToByte(utf16FromString(pw)) +} + +// List retrieves all credentials of the Credentials store. +func List() ([]*Credential, error) { + creds, err := sysCredEnumerate("", true) + if err != nil && errors.Is(err, ErrElementNotFound) { + // Ignore ERROR_NOT_FOUND and return an empty list instead + creds = []*Credential{} + err = nil + } + return creds, err +} + +// FilteredList retrieves the list of credentials from the Credentials store that match the given filter. +// The filter string defines the prefix followed by an asterisk for the `TargetName` attribute of the credentials. +func FilteredList(filter string) ([]*Credential, error) { + creds, err := sysCredEnumerate(filter, false) + if err != nil && errors.Is(err, ErrElementNotFound) { + // Ignore ERROR_NOT_FOUND and return an empty list instead + creds = []*Credential{} + err = nil + } + return creds, err +} diff --git a/vendor/github.com/fatih/color/LICENSE.md b/vendor/github.com/fatih/color/LICENSE.md new file mode 100644 index 000000000..25fdaf639 --- /dev/null +++ b/vendor/github.com/fatih/color/LICENSE.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013 Fatih Arslan + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/fatih/color/README.md b/vendor/github.com/fatih/color/README.md new file mode 100644 index 000000000..be82827ca --- /dev/null +++ b/vendor/github.com/fatih/color/README.md @@ -0,0 +1,176 @@ +# color [![](https://github.com/fatih/color/workflows/build/badge.svg)](https://github.com/fatih/color/actions) [![PkgGoDev](https://pkg.go.dev/badge/github.com/fatih/color)](https://pkg.go.dev/github.com/fatih/color) + +Color lets you use colorized outputs in terms of [ANSI Escape +Codes](http://en.wikipedia.org/wiki/ANSI_escape_code#Colors) in Go (Golang). It +has support for Windows too! The API can be used in several ways, pick one that +suits you. + +![Color](https://user-images.githubusercontent.com/438920/96832689-03b3e000-13f4-11eb-9803-46f4c4de3406.jpg) + +## Install + +```bash +go get github.com/fatih/color +``` + +## Examples + +### Standard colors + +```go +// Print with default helper functions +color.Cyan("Prints text in cyan.") + +// A newline will be appended automatically +color.Blue("Prints %s in blue.", "text") + +// These are using the default foreground colors +color.Red("We have red") +color.Magenta("And many others ..") + +``` + +### Mix and reuse colors + +```go +// Create a new color object +c := color.New(color.FgCyan).Add(color.Underline) +c.Println("Prints cyan text with an underline.") + +// Or just add them to New() +d := color.New(color.FgCyan, color.Bold) +d.Printf("This prints bold cyan %s\n", "too!.") + +// Mix up foreground and background colors, create new mixes! +red := color.New(color.FgRed) + +boldRed := red.Add(color.Bold) +boldRed.Println("This will print text in bold red.") + +whiteBackground := red.Add(color.BgWhite) +whiteBackground.Println("Red text with white background.") +``` + +### Use your own output (io.Writer) + +```go +// Use your own io.Writer output +color.New(color.FgBlue).Fprintln(myWriter, "blue color!") + +blue := color.New(color.FgBlue) +blue.Fprint(writer, "This will print text in blue.") +``` + +### Custom print functions (PrintFunc) + +```go +// Create a custom print function for convenience +red := color.New(color.FgRed).PrintfFunc() +red("Warning") +red("Error: %s", err) + +// Mix up multiple attributes +notice := color.New(color.Bold, color.FgGreen).PrintlnFunc() +notice("Don't forget this...") +``` + +### Custom fprint functions (FprintFunc) + +```go +blue := color.New(color.FgBlue).FprintfFunc() +blue(myWriter, "important notice: %s", stars) + +// Mix up with multiple attributes +success := color.New(color.Bold, color.FgGreen).FprintlnFunc() +success(myWriter, "Don't forget this...") +``` + +### Insert into noncolor strings (SprintFunc) + +```go +// Create SprintXxx functions to mix strings with other non-colorized strings: +yellow := color.New(color.FgYellow).SprintFunc() +red := color.New(color.FgRed).SprintFunc() +fmt.Printf("This is a %s and this is %s.\n", yellow("warning"), red("error")) + +info := color.New(color.FgWhite, color.BgGreen).SprintFunc() +fmt.Printf("This %s rocks!\n", info("package")) + +// Use helper functions +fmt.Println("This", color.RedString("warning"), "should be not neglected.") +fmt.Printf("%v %v\n", color.GreenString("Info:"), "an important message.") + +// Windows supported too! Just don't forget to change the output to color.Output +fmt.Fprintf(color.Output, "Windows support: %s", color.GreenString("PASS")) +``` + +### Plug into existing code + +```go +// Use handy standard colors +color.Set(color.FgYellow) + +fmt.Println("Existing text will now be in yellow") +fmt.Printf("This one %s\n", "too") + +color.Unset() // Don't forget to unset + +// You can mix up parameters +color.Set(color.FgMagenta, color.Bold) +defer color.Unset() // Use it in your function + +fmt.Println("All text will now be bold magenta.") +``` + +### Disable/Enable color + +There might be a case where you want to explicitly disable/enable color output. the +`go-isatty` package will automatically disable color output for non-tty output streams +(for example if the output were piped directly to `less`). + +The `color` package also disables color output if the [`NO_COLOR`](https://no-color.org) environment +variable is set to a non-empty string. + +`Color` has support to disable/enable colors programmatically both globally and +for single color definitions. For example suppose you have a CLI app and a +`-no-color` bool flag. You can easily disable the color output with: + +```go +var flagNoColor = flag.Bool("no-color", false, "Disable color output") + +if *flagNoColor { + color.NoColor = true // disables colorized output +} +``` + +It also has support for single color definitions (local). You can +disable/enable color output on the fly: + +```go +c := color.New(color.FgCyan) +c.Println("Prints cyan text") + +c.DisableColor() +c.Println("This is printed without any color") + +c.EnableColor() +c.Println("This prints again cyan...") +``` + +## GitHub Actions + +To output color in GitHub Actions (or other CI systems that support ANSI colors), make sure to set `color.NoColor = false` so that it bypasses the check for non-tty output streams. + +## Todo + +* Save/Return previous values +* Evaluate fmt.Formatter interface + +## Credits + +* [Fatih Arslan](https://github.com/fatih) +* Windows support via @mattn: [colorable](https://github.com/mattn/go-colorable) + +## License + +The MIT License (MIT) - see [`LICENSE.md`](https://github.com/fatih/color/blob/master/LICENSE.md) for more details diff --git a/vendor/github.com/fatih/color/color.go b/vendor/github.com/fatih/color/color.go new file mode 100644 index 000000000..c4234287d --- /dev/null +++ b/vendor/github.com/fatih/color/color.go @@ -0,0 +1,650 @@ +package color + +import ( + "fmt" + "io" + "os" + "strconv" + "strings" + "sync" + + "github.com/mattn/go-colorable" + "github.com/mattn/go-isatty" +) + +var ( + // NoColor defines if the output is colorized or not. It's dynamically set to + // false or true based on the stdout's file descriptor referring to a terminal + // or not. It's also set to true if the NO_COLOR environment variable is + // set (regardless of its value). This is a global option and affects all + // colors. For more control over each color block use the methods + // DisableColor() individually. + NoColor = noColorIsSet() || os.Getenv("TERM") == "dumb" || + (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) + + // Output defines the standard output of the print functions. By default, + // os.Stdout is used. + Output = colorable.NewColorableStdout() + + // Error defines a color supporting writer for os.Stderr. + Error = colorable.NewColorableStderr() + + // colorsCache is used to reduce the count of created Color objects and + // allows to reuse already created objects with required Attribute. + colorsCache = make(map[Attribute]*Color) + colorsCacheMu sync.Mutex // protects colorsCache +) + +// noColorIsSet returns true if the environment variable NO_COLOR is set to a non-empty string. +func noColorIsSet() bool { + return os.Getenv("NO_COLOR") != "" +} + +// Color defines a custom color object which is defined by SGR parameters. +type Color struct { + params []Attribute + noColor *bool +} + +// Attribute defines a single SGR Code +type Attribute int + +const escape = "\x1b" + +// Base attributes +const ( + Reset Attribute = iota + Bold + Faint + Italic + Underline + BlinkSlow + BlinkRapid + ReverseVideo + Concealed + CrossedOut +) + +const ( + ResetBold Attribute = iota + 22 + ResetItalic + ResetUnderline + ResetBlinking + _ + ResetReversed + ResetConcealed + ResetCrossedOut +) + +var mapResetAttributes map[Attribute]Attribute = map[Attribute]Attribute{ + Bold: ResetBold, + Faint: ResetBold, + Italic: ResetItalic, + Underline: ResetUnderline, + BlinkSlow: ResetBlinking, + BlinkRapid: ResetBlinking, + ReverseVideo: ResetReversed, + Concealed: ResetConcealed, + CrossedOut: ResetCrossedOut, +} + +// Foreground text colors +const ( + FgBlack Attribute = iota + 30 + FgRed + FgGreen + FgYellow + FgBlue + FgMagenta + FgCyan + FgWhite +) + +// Foreground Hi-Intensity text colors +const ( + FgHiBlack Attribute = iota + 90 + FgHiRed + FgHiGreen + FgHiYellow + FgHiBlue + FgHiMagenta + FgHiCyan + FgHiWhite +) + +// Background text colors +const ( + BgBlack Attribute = iota + 40 + BgRed + BgGreen + BgYellow + BgBlue + BgMagenta + BgCyan + BgWhite +) + +// Background Hi-Intensity text colors +const ( + BgHiBlack Attribute = iota + 100 + BgHiRed + BgHiGreen + BgHiYellow + BgHiBlue + BgHiMagenta + BgHiCyan + BgHiWhite +) + +// New returns a newly created color object. +func New(value ...Attribute) *Color { + c := &Color{ + params: make([]Attribute, 0), + } + + if noColorIsSet() { + c.noColor = boolPtr(true) + } + + c.Add(value...) + return c +} + +// Set sets the given parameters immediately. It will change the color of +// output with the given SGR parameters until color.Unset() is called. +func Set(p ...Attribute) *Color { + c := New(p...) + c.Set() + return c +} + +// Unset resets all escape attributes and clears the output. Usually should +// be called after Set(). +func Unset() { + if NoColor { + return + } + + fmt.Fprintf(Output, "%s[%dm", escape, Reset) +} + +// Set sets the SGR sequence. +func (c *Color) Set() *Color { + if c.isNoColorSet() { + return c + } + + fmt.Fprint(Output, c.format()) + return c +} + +func (c *Color) unset() { + if c.isNoColorSet() { + return + } + + Unset() +} + +// SetWriter is used to set the SGR sequence with the given io.Writer. This is +// a low-level function, and users should use the higher-level functions, such +// as color.Fprint, color.Print, etc. +func (c *Color) SetWriter(w io.Writer) *Color { + if c.isNoColorSet() { + return c + } + + fmt.Fprint(w, c.format()) + return c +} + +// UnsetWriter resets all escape attributes and clears the output with the give +// io.Writer. Usually should be called after SetWriter(). +func (c *Color) UnsetWriter(w io.Writer) { + if c.isNoColorSet() { + return + } + + if NoColor { + return + } + + fmt.Fprintf(w, "%s[%dm", escape, Reset) +} + +// Add is used to chain SGR parameters. Use as many as parameters to combine +// and create custom color objects. Example: Add(color.FgRed, color.Underline). +func (c *Color) Add(value ...Attribute) *Color { + c.params = append(c.params, value...) + return c +} + +// Fprint formats using the default formats for its operands and writes to w. +// Spaces are added between operands when neither is a string. +// It returns the number of bytes written and any write error encountered. +// On Windows, users should wrap w with colorable.NewColorable() if w is of +// type *os.File. +func (c *Color) Fprint(w io.Writer, a ...interface{}) (n int, err error) { + c.SetWriter(w) + defer c.UnsetWriter(w) + + return fmt.Fprint(w, a...) +} + +// Print formats using the default formats for its operands and writes to +// standard output. Spaces are added between operands when neither is a +// string. It returns the number of bytes written and any write error +// encountered. This is the standard fmt.Print() method wrapped with the given +// color. +func (c *Color) Print(a ...interface{}) (n int, err error) { + c.Set() + defer c.unset() + + return fmt.Fprint(Output, a...) +} + +// Fprintf formats according to a format specifier and writes to w. +// It returns the number of bytes written and any write error encountered. +// On Windows, users should wrap w with colorable.NewColorable() if w is of +// type *os.File. +func (c *Color) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { + c.SetWriter(w) + defer c.UnsetWriter(w) + + return fmt.Fprintf(w, format, a...) +} + +// Printf formats according to a format specifier and writes to standard output. +// It returns the number of bytes written and any write error encountered. +// This is the standard fmt.Printf() method wrapped with the given color. +func (c *Color) Printf(format string, a ...interface{}) (n int, err error) { + c.Set() + defer c.unset() + + return fmt.Fprintf(Output, format, a...) +} + +// Fprintln formats using the default formats for its operands and writes to w. +// Spaces are always added between operands and a newline is appended. +// On Windows, users should wrap w with colorable.NewColorable() if w is of +// type *os.File. +func (c *Color) Fprintln(w io.Writer, a ...interface{}) (n int, err error) { + return fmt.Fprintln(w, c.wrap(fmt.Sprint(a...))) +} + +// Println formats using the default formats for its operands and writes to +// standard output. Spaces are always added between operands and a newline is +// appended. It returns the number of bytes written and any write error +// encountered. This is the standard fmt.Print() method wrapped with the given +// color. +func (c *Color) Println(a ...interface{}) (n int, err error) { + return fmt.Fprintln(Output, c.wrap(fmt.Sprint(a...))) +} + +// Sprint is just like Print, but returns a string instead of printing it. +func (c *Color) Sprint(a ...interface{}) string { + return c.wrap(fmt.Sprint(a...)) +} + +// Sprintln is just like Println, but returns a string instead of printing it. +func (c *Color) Sprintln(a ...interface{}) string { + return fmt.Sprintln(c.Sprint(a...)) +} + +// Sprintf is just like Printf, but returns a string instead of printing it. +func (c *Color) Sprintf(format string, a ...interface{}) string { + return c.wrap(fmt.Sprintf(format, a...)) +} + +// FprintFunc returns a new function that prints the passed arguments as +// colorized with color.Fprint(). +func (c *Color) FprintFunc() func(w io.Writer, a ...interface{}) { + return func(w io.Writer, a ...interface{}) { + c.Fprint(w, a...) + } +} + +// PrintFunc returns a new function that prints the passed arguments as +// colorized with color.Print(). +func (c *Color) PrintFunc() func(a ...interface{}) { + return func(a ...interface{}) { + c.Print(a...) + } +} + +// FprintfFunc returns a new function that prints the passed arguments as +// colorized with color.Fprintf(). +func (c *Color) FprintfFunc() func(w io.Writer, format string, a ...interface{}) { + return func(w io.Writer, format string, a ...interface{}) { + c.Fprintf(w, format, a...) + } +} + +// PrintfFunc returns a new function that prints the passed arguments as +// colorized with color.Printf(). +func (c *Color) PrintfFunc() func(format string, a ...interface{}) { + return func(format string, a ...interface{}) { + c.Printf(format, a...) + } +} + +// FprintlnFunc returns a new function that prints the passed arguments as +// colorized with color.Fprintln(). +func (c *Color) FprintlnFunc() func(w io.Writer, a ...interface{}) { + return func(w io.Writer, a ...interface{}) { + c.Fprintln(w, a...) + } +} + +// PrintlnFunc returns a new function that prints the passed arguments as +// colorized with color.Println(). +func (c *Color) PrintlnFunc() func(a ...interface{}) { + return func(a ...interface{}) { + c.Println(a...) + } +} + +// SprintFunc returns a new function that returns colorized strings for the +// given arguments with fmt.Sprint(). Useful to put into or mix into other +// string. Windows users should use this in conjunction with color.Output, example: +// +// put := New(FgYellow).SprintFunc() +// fmt.Fprintf(color.Output, "This is a %s", put("warning")) +func (c *Color) SprintFunc() func(a ...interface{}) string { + return func(a ...interface{}) string { + return c.wrap(fmt.Sprint(a...)) + } +} + +// SprintfFunc returns a new function that returns colorized strings for the +// given arguments with fmt.Sprintf(). Useful to put into or mix into other +// string. Windows users should use this in conjunction with color.Output. +func (c *Color) SprintfFunc() func(format string, a ...interface{}) string { + return func(format string, a ...interface{}) string { + return c.wrap(fmt.Sprintf(format, a...)) + } +} + +// SprintlnFunc returns a new function that returns colorized strings for the +// given arguments with fmt.Sprintln(). Useful to put into or mix into other +// string. Windows users should use this in conjunction with color.Output. +func (c *Color) SprintlnFunc() func(a ...interface{}) string { + return func(a ...interface{}) string { + return fmt.Sprintln(c.Sprint(a...)) + } +} + +// sequence returns a formatted SGR sequence to be plugged into a "\x1b[...m" +// an example output might be: "1;36" -> bold cyan +func (c *Color) sequence() string { + format := make([]string, len(c.params)) + for i, v := range c.params { + format[i] = strconv.Itoa(int(v)) + } + + return strings.Join(format, ";") +} + +// wrap wraps the s string with the colors attributes. The string is ready to +// be printed. +func (c *Color) wrap(s string) string { + if c.isNoColorSet() { + return s + } + + return c.format() + s + c.unformat() +} + +func (c *Color) format() string { + return fmt.Sprintf("%s[%sm", escape, c.sequence()) +} + +func (c *Color) unformat() string { + //return fmt.Sprintf("%s[%dm", escape, Reset) + //for each element in sequence let's use the speficic reset escape, ou the generic one if not found + format := make([]string, len(c.params)) + for i, v := range c.params { + format[i] = strconv.Itoa(int(Reset)) + ra, ok := mapResetAttributes[v] + if ok { + format[i] = strconv.Itoa(int(ra)) + } + } + + return fmt.Sprintf("%s[%sm", escape, strings.Join(format, ";")) +} + +// DisableColor disables the color output. Useful to not change any existing +// code and still being able to output. Can be used for flags like +// "--no-color". To enable back use EnableColor() method. +func (c *Color) DisableColor() { + c.noColor = boolPtr(true) +} + +// EnableColor enables the color output. Use it in conjunction with +// DisableColor(). Otherwise, this method has no side effects. +func (c *Color) EnableColor() { + c.noColor = boolPtr(false) +} + +func (c *Color) isNoColorSet() bool { + // check first if we have user set action + if c.noColor != nil { + return *c.noColor + } + + // if not return the global option, which is disabled by default + return NoColor +} + +// Equals returns a boolean value indicating whether two colors are equal. +func (c *Color) Equals(c2 *Color) bool { + if c == nil && c2 == nil { + return true + } + if c == nil || c2 == nil { + return false + } + if len(c.params) != len(c2.params) { + return false + } + + for _, attr := range c.params { + if !c2.attrExists(attr) { + return false + } + } + + return true +} + +func (c *Color) attrExists(a Attribute) bool { + for _, attr := range c.params { + if attr == a { + return true + } + } + + return false +} + +func boolPtr(v bool) *bool { + return &v +} + +func getCachedColor(p Attribute) *Color { + colorsCacheMu.Lock() + defer colorsCacheMu.Unlock() + + c, ok := colorsCache[p] + if !ok { + c = New(p) + colorsCache[p] = c + } + + return c +} + +func colorPrint(format string, p Attribute, a ...interface{}) { + c := getCachedColor(p) + + if !strings.HasSuffix(format, "\n") { + format += "\n" + } + + if len(a) == 0 { + c.Print(format) + } else { + c.Printf(format, a...) + } +} + +func colorString(format string, p Attribute, a ...interface{}) string { + c := getCachedColor(p) + + if len(a) == 0 { + return c.SprintFunc()(format) + } + + return c.SprintfFunc()(format, a...) +} + +// Black is a convenient helper function to print with black foreground. A +// newline is appended to format by default. +func Black(format string, a ...interface{}) { colorPrint(format, FgBlack, a...) } + +// Red is a convenient helper function to print with red foreground. A +// newline is appended to format by default. +func Red(format string, a ...interface{}) { colorPrint(format, FgRed, a...) } + +// Green is a convenient helper function to print with green foreground. A +// newline is appended to format by default. +func Green(format string, a ...interface{}) { colorPrint(format, FgGreen, a...) } + +// Yellow is a convenient helper function to print with yellow foreground. +// A newline is appended to format by default. +func Yellow(format string, a ...interface{}) { colorPrint(format, FgYellow, a...) } + +// Blue is a convenient helper function to print with blue foreground. A +// newline is appended to format by default. +func Blue(format string, a ...interface{}) { colorPrint(format, FgBlue, a...) } + +// Magenta is a convenient helper function to print with magenta foreground. +// A newline is appended to format by default. +func Magenta(format string, a ...interface{}) { colorPrint(format, FgMagenta, a...) } + +// Cyan is a convenient helper function to print with cyan foreground. A +// newline is appended to format by default. +func Cyan(format string, a ...interface{}) { colorPrint(format, FgCyan, a...) } + +// White is a convenient helper function to print with white foreground. A +// newline is appended to format by default. +func White(format string, a ...interface{}) { colorPrint(format, FgWhite, a...) } + +// BlackString is a convenient helper function to return a string with black +// foreground. +func BlackString(format string, a ...interface{}) string { return colorString(format, FgBlack, a...) } + +// RedString is a convenient helper function to return a string with red +// foreground. +func RedString(format string, a ...interface{}) string { return colorString(format, FgRed, a...) } + +// GreenString is a convenient helper function to return a string with green +// foreground. +func GreenString(format string, a ...interface{}) string { return colorString(format, FgGreen, a...) } + +// YellowString is a convenient helper function to return a string with yellow +// foreground. +func YellowString(format string, a ...interface{}) string { return colorString(format, FgYellow, a...) } + +// BlueString is a convenient helper function to return a string with blue +// foreground. +func BlueString(format string, a ...interface{}) string { return colorString(format, FgBlue, a...) } + +// MagentaString is a convenient helper function to return a string with magenta +// foreground. +func MagentaString(format string, a ...interface{}) string { + return colorString(format, FgMagenta, a...) +} + +// CyanString is a convenient helper function to return a string with cyan +// foreground. +func CyanString(format string, a ...interface{}) string { return colorString(format, FgCyan, a...) } + +// WhiteString is a convenient helper function to return a string with white +// foreground. +func WhiteString(format string, a ...interface{}) string { return colorString(format, FgWhite, a...) } + +// HiBlack is a convenient helper function to print with hi-intensity black foreground. A +// newline is appended to format by default. +func HiBlack(format string, a ...interface{}) { colorPrint(format, FgHiBlack, a...) } + +// HiRed is a convenient helper function to print with hi-intensity red foreground. A +// newline is appended to format by default. +func HiRed(format string, a ...interface{}) { colorPrint(format, FgHiRed, a...) } + +// HiGreen is a convenient helper function to print with hi-intensity green foreground. A +// newline is appended to format by default. +func HiGreen(format string, a ...interface{}) { colorPrint(format, FgHiGreen, a...) } + +// HiYellow is a convenient helper function to print with hi-intensity yellow foreground. +// A newline is appended to format by default. +func HiYellow(format string, a ...interface{}) { colorPrint(format, FgHiYellow, a...) } + +// HiBlue is a convenient helper function to print with hi-intensity blue foreground. A +// newline is appended to format by default. +func HiBlue(format string, a ...interface{}) { colorPrint(format, FgHiBlue, a...) } + +// HiMagenta is a convenient helper function to print with hi-intensity magenta foreground. +// A newline is appended to format by default. +func HiMagenta(format string, a ...interface{}) { colorPrint(format, FgHiMagenta, a...) } + +// HiCyan is a convenient helper function to print with hi-intensity cyan foreground. A +// newline is appended to format by default. +func HiCyan(format string, a ...interface{}) { colorPrint(format, FgHiCyan, a...) } + +// HiWhite is a convenient helper function to print with hi-intensity white foreground. A +// newline is appended to format by default. +func HiWhite(format string, a ...interface{}) { colorPrint(format, FgHiWhite, a...) } + +// HiBlackString is a convenient helper function to return a string with hi-intensity black +// foreground. +func HiBlackString(format string, a ...interface{}) string { + return colorString(format, FgHiBlack, a...) +} + +// HiRedString is a convenient helper function to return a string with hi-intensity red +// foreground. +func HiRedString(format string, a ...interface{}) string { return colorString(format, FgHiRed, a...) } + +// HiGreenString is a convenient helper function to return a string with hi-intensity green +// foreground. +func HiGreenString(format string, a ...interface{}) string { + return colorString(format, FgHiGreen, a...) +} + +// HiYellowString is a convenient helper function to return a string with hi-intensity yellow +// foreground. +func HiYellowString(format string, a ...interface{}) string { + return colorString(format, FgHiYellow, a...) +} + +// HiBlueString is a convenient helper function to return a string with hi-intensity blue +// foreground. +func HiBlueString(format string, a ...interface{}) string { return colorString(format, FgHiBlue, a...) } + +// HiMagentaString is a convenient helper function to return a string with hi-intensity magenta +// foreground. +func HiMagentaString(format string, a ...interface{}) string { + return colorString(format, FgHiMagenta, a...) +} + +// HiCyanString is a convenient helper function to return a string with hi-intensity cyan +// foreground. +func HiCyanString(format string, a ...interface{}) string { return colorString(format, FgHiCyan, a...) } + +// HiWhiteString is a convenient helper function to return a string with hi-intensity white +// foreground. +func HiWhiteString(format string, a ...interface{}) string { + return colorString(format, FgHiWhite, a...) +} diff --git a/vendor/github.com/fatih/color/color_windows.go b/vendor/github.com/fatih/color/color_windows.go new file mode 100644 index 000000000..be01c558e --- /dev/null +++ b/vendor/github.com/fatih/color/color_windows.go @@ -0,0 +1,19 @@ +package color + +import ( + "os" + + "golang.org/x/sys/windows" +) + +func init() { + // Opt-in for ansi color support for current process. + // https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#output-sequences + var outMode uint32 + out := windows.Handle(os.Stdout.Fd()) + if err := windows.GetConsoleMode(out, &outMode); err != nil { + return + } + outMode |= windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING + _ = windows.SetConsoleMode(out, outMode) +} diff --git a/vendor/github.com/fatih/color/doc.go b/vendor/github.com/fatih/color/doc.go new file mode 100644 index 000000000..9491ad541 --- /dev/null +++ b/vendor/github.com/fatih/color/doc.go @@ -0,0 +1,134 @@ +/* +Package color is an ANSI color package to output colorized or SGR defined +output to the standard output. The API can be used in several way, pick one +that suits you. + +Use simple and default helper functions with predefined foreground colors: + + color.Cyan("Prints text in cyan.") + + // a newline will be appended automatically + color.Blue("Prints %s in blue.", "text") + + // More default foreground colors.. + color.Red("We have red") + color.Yellow("Yellow color too!") + color.Magenta("And many others ..") + + // Hi-intensity colors + color.HiGreen("Bright green color.") + color.HiBlack("Bright black means gray..") + color.HiWhite("Shiny white color!") + +However, there are times when custom color mixes are required. Below are some +examples to create custom color objects and use the print functions of each +separate color object. + + // Create a new color object + c := color.New(color.FgCyan).Add(color.Underline) + c.Println("Prints cyan text with an underline.") + + // Or just add them to New() + d := color.New(color.FgCyan, color.Bold) + d.Printf("This prints bold cyan %s\n", "too!.") + + + // Mix up foreground and background colors, create new mixes! + red := color.New(color.FgRed) + + boldRed := red.Add(color.Bold) + boldRed.Println("This will print text in bold red.") + + whiteBackground := red.Add(color.BgWhite) + whiteBackground.Println("Red text with White background.") + + // Use your own io.Writer output + color.New(color.FgBlue).Fprintln(myWriter, "blue color!") + + blue := color.New(color.FgBlue) + blue.Fprint(myWriter, "This will print text in blue.") + +You can create PrintXxx functions to simplify even more: + + // Create a custom print function for convenient + red := color.New(color.FgRed).PrintfFunc() + red("warning") + red("error: %s", err) + + // Mix up multiple attributes + notice := color.New(color.Bold, color.FgGreen).PrintlnFunc() + notice("don't forget this...") + +You can also FprintXxx functions to pass your own io.Writer: + + blue := color.New(FgBlue).FprintfFunc() + blue(myWriter, "important notice: %s", stars) + + // Mix up with multiple attributes + success := color.New(color.Bold, color.FgGreen).FprintlnFunc() + success(myWriter, don't forget this...") + +Or create SprintXxx functions to mix strings with other non-colorized strings: + + yellow := New(FgYellow).SprintFunc() + red := New(FgRed).SprintFunc() + + fmt.Printf("this is a %s and this is %s.\n", yellow("warning"), red("error")) + + info := New(FgWhite, BgGreen).SprintFunc() + fmt.Printf("this %s rocks!\n", info("package")) + +Windows support is enabled by default. All Print functions work as intended. +However, only for color.SprintXXX functions, user should use fmt.FprintXXX and +set the output to color.Output: + + fmt.Fprintf(color.Output, "Windows support: %s", color.GreenString("PASS")) + + info := New(FgWhite, BgGreen).SprintFunc() + fmt.Fprintf(color.Output, "this %s rocks!\n", info("package")) + +Using with existing code is possible. Just use the Set() method to set the +standard output to the given parameters. That way a rewrite of an existing +code is not required. + + // Use handy standard colors. + color.Set(color.FgYellow) + + fmt.Println("Existing text will be now in Yellow") + fmt.Printf("This one %s\n", "too") + + color.Unset() // don't forget to unset + + // You can mix up parameters + color.Set(color.FgMagenta, color.Bold) + defer color.Unset() // use it in your function + + fmt.Println("All text will be now bold magenta.") + +There might be a case where you want to disable color output (for example to +pipe the standard output of your app to somewhere else). `Color` has support to +disable colors both globally and for single color definition. For example +suppose you have a CLI app and a `--no-color` bool flag. You can easily disable +the color output with: + + var flagNoColor = flag.Bool("no-color", false, "Disable color output") + + if *flagNoColor { + color.NoColor = true // disables colorized output + } + +You can also disable the color by setting the NO_COLOR environment variable to any value. + +It also has support for single color definitions (local). You can +disable/enable color output on the fly: + + c := color.New(color.FgCyan) + c.Println("Prints cyan text") + + c.DisableColor() + c.Println("This is printed without any color") + + c.EnableColor() + c.Println("This prints again cyan...") +*/ +package color diff --git a/vendor/github.com/godbus/dbus/v5/CONTRIBUTING.md b/vendor/github.com/godbus/dbus/v5/CONTRIBUTING.md new file mode 100644 index 000000000..c88f9b2bd --- /dev/null +++ b/vendor/github.com/godbus/dbus/v5/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# How to Contribute + +## Getting Started + +- Fork the repository on GitHub +- Read the [README](README.markdown) for build and test instructions +- Play with the project, submit bugs, submit patches! + +## Contribution Flow + +This is a rough outline of what a contributor's workflow looks like: + +- Create a topic branch from where you want to base your work (usually master). +- Make commits of logical units. +- Make sure your commit messages are in the proper format (see below). +- Push your changes to a topic branch in your fork of the repository. +- Make sure the tests pass, and add any new tests as appropriate. +- Submit a pull request to the original repository. + +Thanks for your contributions! + +### Format of the Commit Message + +We follow a rough convention for commit messages that is designed to answer two +questions: what changed and why. The subject line should feature the what and +the body of the commit should describe the why. + +``` +scripts: add the test-cluster command + +this uses tmux to setup a test cluster that you can easily kill and +start for debugging. + +Fixes #38 +``` + +The format can be described more formally as follows: + +``` +: + + + +