diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index caf507a..3cfe3a1 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -17,7 +17,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.22 + go-version: stable id: go # Look for a CLI that's made for this PR - name: Fetch built CLI @@ -50,7 +50,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.22 + go-version: stable - name: Check out the code uses: actions/checkout@v4 - name: Fetch built CLI @@ -61,4 +61,4 @@ jobs: key: commitizen-${{ github.event.pull_request.number }}-${{ github.sha }} - name: Run E2E test run: | - GOPATH=~/go make e2e NO_TTY=1 + GOPATH=~/go make e2e diff --git a/.github/workflows/grype.yaml b/.github/workflows/grype.yaml index 5ce578b..2342527 100644 --- a/.github/workflows/grype.yaml +++ b/.github/workflows/grype.yaml @@ -20,7 +20,7 @@ jobs: contents: read steps: - uses: actions/checkout@v4 - - uses: anchore/scan-action@v3 + - uses: anchore/scan-action@v6 with: path: "." fail-build: true \ No newline at end of file diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index d2ef6c2..f731df1 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -17,7 +17,7 @@ jobs: golangci: strategy: matrix: - go: [ '1.20', '1.21', '1.22' ] + go: [ '1.22', '1.23', '1.24' ] os: [ ubuntu-latest, windows-latest ] permissions: contents: read # for actions/checkout to fetch code @@ -31,7 +31,7 @@ jobs: go-version: stable # get the latest stable version from the go-versions repository manifest. cache: false - name: golangci-lint - uses: golangci/golangci-lint-action@v4 + uses: golangci/golangci-lint-action@v8 with: args: --timeout=10m version: latest \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 1e1b0dd..c226565 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -14,13 +14,13 @@ jobs: fetch-depth: 0 # it is required fot the changelog to work correctly - uses: actions/setup-go@v5 with: - go-version: 1.22 + go-version: stable - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v5 + uses: goreleaser/goreleaser-action@v6 env: GITHUB_TOKEN: ${{ secrets.PAT }} with: distribution: goreleaser version: latest - args: release --debug --clean + args: release --verbose --clean diff --git a/.golangci.yaml b/.golangci.yaml index 8b9e289..46efb95 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,31 +1,47 @@ +version: "2" run: - # Include test files or not. - # Default: true tests: false - linters: - disable-all: true + default: none enable: - - misspell - - govet - - staticcheck + - dupl - errcheck - - unparam + - gocyclo + - gosec + - govet - ineffassign + - misspell - nakedret - - gocyclo - - dupl - - goimports - revive - - gosec - - gosimple - - typecheck + - staticcheck + - unparam - unused - -linters-settings: - gofmt: - simplify: true - goimports: - local-prefixes: github.com/shipengqi/commitizen - dupl: - threshold: 600 \ No newline at end of file + settings: + dupl: + threshold: 600 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - goimports + settings: + gofmt: + simplify: true + goimports: + local-prefixes: + - github.com/shipengqi/commitizen + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/.goreleaser.yaml b/.goreleaser.yaml index f22347b..d417ae3 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -76,6 +76,14 @@ release: footer: | **Full Changelog**: https://github.com/shipengqi/commitizen/compare/{{ .PreviousTag }}...{{ .Tag }} +scoops: + - repository: + owner: shipengqi + name: scoop-bucket + directory: bucket + homepage: https://github.com/shipengqi/commitizen + description: The commitizen command line utility, without nodejs. Forked from commitizen-go, fixes some issues of commitizen-go and supports more new features. + license: MIT # modelines, feel free to remove those if you don't want/use them: # yaml-language-server: $schema=https://goreleaser.com/static/schema.json diff --git a/Makefile b/Makefile index 96ed370..dd77a04 100644 --- a/Makefile +++ b/Makefile @@ -24,8 +24,6 @@ Options: This option is available when using: make release V Set to 1 enable verbose build. Default is 0. DEBUG Whether to generate debug symbols. Default is 0. - NO_TTY Make sure that the TTY (terminal) is never used for - any output. Default is 0. endef export USAGE_OPTIONS diff --git a/README.md b/README.md index e40246e..50c9d21 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,14 @@ Command line utility to standardize git commit messages, golang version. Forked Fixes some issues of commitizen-go and supports more new features. -![demo](https://github.com/shipengqi/illustrations/blob/ebe8786a60c6467edb3122723d74d22f639fb216/commitizen/demo.gif?raw=true) +![demo](https://github.com/shipengqi/illustrations/blob/e0d588dd70551344f0394cbf6671b15ae22e7635/commitizen/demo.gif?raw=true) ## Features -- Multi-template support -- Support more options of `git commit` -- Use [bubbletea](https://github.com/charmbracelet/bubbletea) instead of [survey](https://github.com/AlecAivazis/survey) ([survey](https://github.com/AlecAivazis/survey) is no longer maintained). -- Better unit tests. +- Multi-template support. +- More powerful and flexible template. +- Support more options of `git commit`. +- Use [huh](https://github.com/charmbracelet/huh) instead of [survey](https://github.com/AlecAivazis/survey) ([survey](https://github.com/AlecAivazis/survey) is no longer maintained). ## Getting Started @@ -40,6 +40,10 @@ Git Commit flags: override author for commit --date string override date for commit + --git-flag strings + git flags, e.g. --git-flag="--branch" + -n, --no-verify + bypass pre-commit and commit-msg hooks. -q, --quiet suppress summary after successful commit -s, --signoff @@ -59,6 +63,8 @@ Commitizen flags: Use "commitizen [command] --help" for more information about a command. ``` +> To use more Git flags, you can use the '--git-flag' flag. Please do not conflict with other Git commit flags. + Commit with commitizen: ``` @@ -67,11 +73,21 @@ $ git cz ## Installation +### Scoop (Windows) + +```bash +$ scoop bucket add czbucket https://github.com/shipengqi/scoop-bucket.git +$ scoop install commitizen + +$ commitizen.exe init +``` + ### From the Binary Releases Download the pre-compiled binaries from the [releases page](https://github.com/shipengqi/commitizen/releases) and copy them to the desired location. Then install this tool to git-core as git-cz: + ``` $ commitizen init ``` @@ -97,80 +113,308 @@ $ make && ./_output/$(GOOS)/$(GOARCH)/bin/commitizen init ## Configuration -You can set configuration file that `.git-czrc` at repository root or home directory. The configuration file that located in repository root have a priority over the one in home directory. The format is the same as the following: +You can set configuration file that `.czrc` at repository root, home directory, or the `$XDG_CONFIG_HOME/commitizen` directory. + +commitizen uses the following precedence order. Each item takes precedence over the item below it: + +- per-project config file (`/path/to/my/project/.czrc`) +- per-user config file (`~/.czrc`) +- `$XDG_CONFIG_HOME` config file (`$XDG_CONFIG_HOME/commitizen/.czrc`) + +The format is the same as the following: ```yaml -name: my-default -default: true # (optional) If true, this template will be used as the default template, note that there can only be one default template +name: default +default: true +groups: + - name: hasbreaking + depends_on: + and_conditions: + - parameter_name: page2.isbreaking + value_equals: true + - name: nobreaking + depends_on: + and_conditions: + - parameter_name: page2.isbreaking + value_equals: false items: - name: type - desc: "Select the type of change that you're committing:" - type: select + group: page1 + label: "Select the type of change that you're committing:" + type: list options: - - name: feat - desc: "A new feature" - - name: fix - desc: "A bug fix" - - name: docs - desc: "Documentation only changes" - - name: test - desc: "Adding missing tests" - - name: WIP - desc: "Work in progress" - - name: chore - desc: "Changes to the build process or auxiliary tools\n and libraries such as documentation generation" - - name: style - desc: "Changes that do not affect the meaning of the code\n (white-space, formatting, missing semi-colons, etc)" - - name: refactor - desc: "A code change that neither fixes a bug nor adds a feature" - - name: perf - desc: "A code change that improves performance" - - name: revert - desc: "Revert to a commit" + - value: feat + key: "feat: A new feature" + - value: fix + key: "fix: A bug fix" + - value: docs + key: "docs: Documentation only changes" + - value: test + key: "test: Adding missing or correcting existing tests" + - value: chore + key: "chore: Changes to the build process or auxiliary tools and libraries such as documentation generation" + - value: style + key: "style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)" + - value: refactor + key: "refactor: A code change that neither fixes a bug nor adds a feature" + - value: perf + key: "perf: A code change that improves performance" + - value: revert + key: "revert: Reverts a previous commit" - name: scope - desc: "Scope. Could be anything specifying place of the commit change:" - type: input + group: page2 + label: "Scope. What is the scope of this change? (class or file name):" + type: string + trim: true - name: subject - desc: "Subject. Concise description of the changes. Imperative, lower case and no final dot:" - type: input - required: true # (optional) If true, enable a validator that requires the control have a non-empty value. - - name: body - desc: "Body. Motivation for the change and contrast this with previous behavior:" - type: textarea + group: page2 + label: "Subject. Write a short and imperative summary of the code change (lower case and no period):" + type: string + required: true + trim: true + - name: isbreaking + group: page2 + label: "Are there any breaking changes?" + type: boolean + - name: hasbreakingbody + group: hasbreaking + label: "A BREAKING CHANGE commit requires a body. Provide additional contextual information about the code changes:" + type: text + required: true + - name: nobreakingbody + group: nobreaking + label: "Body. Provide additional contextual information about the code changes:" + type: text - name: footer - desc: "Footer. Information about Breaking Changes and reference issues that this commit closes:" - type: textarea -format: "{{.type}}{{with .scope}}({{.}}){{end}}: {{.subject}}{{with .body}}\n\n{{.}}{{end}}{{with .footer}}\n\n{{.}}{{end}}"` + group: page3 + label: "Footer. Information about Breaking Changes and reference issues that this commit closes:" + type: text +format: "{{.type}}{{with .scope}}({{.}}){{end}}: {{.subject}}{{with .hasbreakingbody}}\n\n{{.}}{{end}}{{with .nobreakingbody}}\n\n{{.}}{{end}}{{with .footer}}\n\n{{.}}{{end}}" ``` +### Default + +Optional. If true, the template will be used as the default template, note that there can only be one default template. + +### Format + Commit message `format`: ``` format: "{{.type}}{{with .scope}}({{.}}){{end}}: {{.subject}}{{with .body}}\n\n{{.}}{{end}}{{with .footer}}\n\n{{.}}{{end}}" ``` +### Items + +#### Common Item Properties + +| Property | Required | Default Value | Description | +|:------------|:---------|:--------------|:--------------------------------------------------------------------------------------------------------------------| +| name | yes | - | Unique identifier for the item. | +| label | yes | - | This will be used as the label for the input field in the UI. | +| type | yes | - | The type of item. Determines which UI widget is shown. See the Item Types section to see all the different options. | +| group | no | - | The name of the group this item belongs to. Separates items into groups (you can think of groups as pages). | +| description | no | - | A short description of the item for user guidance. This will be displayed along with the input field. | + +#### Item Types + +- string +- text +- integer +- boolean +- secret +- list +- multi_list + +#### string + +`string` are single line text parameters. + +Properties: + +| Property | Required | Default Value | Description | +|:--------------|:---------|:--------------|:-------------------------------------------------------------------------------------------------------------| +| required | no | `false` | Whether a string value is required or not. | +| fqdn | no | `false` | Add a preset FQDN regex to validate string. | +| ip | no | `false` | Add a preset IPv4/IPv6 regex to validate string. | +| trim | no | `false` | If true, will remove the leading and trailing blank characters before submit. | +| default_value | no | - | The default value for this item. | +| regex | no | - | A regex used to validate the string. | +| min_length | no | - | The minimum length of the string. If the value is not required and no value has been given, this is ignored. | +| max_length | no | - | The maximum length of the string. | + +#### text + +Properties: + +| Property | Required | Default Value | Description | +|:--------------|:---------|:--------------|:-----------------------------------------------------------------------------------------------------------| +| required | no | `false` | Whether the text is required or not. | +| height | no | 5 | The height of the text. | +| default_value | no | - | The default value for this item. | +| regex | no | - | A regex used to validate the text. | +| min_length | no | - | The minimum length of the text. If the value is not required and no value has been given, this is ignored. | +| max_length | no | - | The maximum length of the text. | + +#### integer + +`integer` is a number. + +Properties: + +| Property | Required | Default Value | Description | +|:--------------|:---------|:--------------|:----------------------------------------| +| required | no | `false` | Whether the integer is required or not. | +| default_value | no | - | The default value for this item. | +| min | no | - | The minimum value allowed. | +| max | no | - | The maximum value allowed. | + +#### boolean + +`boolean` are true or false values. + +Properties: + +| Property | Required | Default Value | Description | +|:--------------|:---------|:--------------|:---------------------------------| +| default_value | no | - | The default value for this item. | + +#### secret + +`secret` is used for sensitive data that should not be echoed in the UI, for example, passwords. + +Properties: + +| Property | Required | Default Value | Description | +|:--------------|:---------|:--------------|:-------------------------------------------------------------------------------------------------------------| +| required | no | `false` | Whether the secret is required or not. | +| trim | no | `false` | If true, will remove the leading and trailing blank characters before submit. | +| default_value | no | - | The default value for this item. | +| regex | no | - | A regex used to validate the secret. | +| min_length | no | - | The minimum length of the secret. If the value is not required and no value has been given, this is ignored. | +| max_length | no | - | The maximum length of the secret. | + +#### list + +`list` is predefined lists of values that can be picked by the user. + +Properties: + +| Property | Required | Default Value | Description | +|:--------------|:---------|:--------------|:------------------------------------------------------------------------------------------------------| +| required | no | `false` | Whether a string value is required or not. | +| default_value | no | - | The default value for this item. | +| options | yes | - | The list of options to choose from. | +| height | no | - | The height of the list. If the number of options exceeds the height, the list will become scrollable. | + +#### multi_list + +Similar to `list`, but with multiple selection. + +Properties: + +| Property | Required | Default Value | Description | +|:--------------|:---------|:--------------|:------------------------------------------------------------------------------------------------------| +| required | no | `false` | Whether a string value is required or not. | +| default_value | no | - | A list of default selection values. | +| options | yes | - | The list of options to choose from. | +| limit | no | `false` | The limit of the multiple selection list. | +| height | no | - | The height of the list. If the number of options exceeds the height, the list will become scrollable. | + +#### list/multi_list Options + +Properties: + +| Property | Required | Description | +|:---------|:---------|:---------------------------------| +| key | yes | The message shown in the UI. | +| value | yes | Unique identifier for the value. | + +### Groups (Optional) + +Group Properties: + +| Property | Required | Default Value | Description | +|:-----------|:---------|:--------------|:-----------------------------------------------------------------------------------------| +| name | yes | - | Unique identifier for the property. | +| depends_on | no | - | If this group should only be shown when a specific condition is met on another property. | + +DependsOn Properties: + +| Property | Required | Default Value | Description | +|:---------------|:---------|:--------------|:---------------------------------------------------------------------------------------------| +| or_conditions | no | `[]` | The list of conditions in which at least one must be satisfied for the property to be shown. | +| and_conditions | no | `[]` | The list of conditions in which all must be satisfied for the property to be shown. | + +DependsOn Conditions: + +- ValueEqualsCondition +- ValueNotEqualsCondition +- ValueContainsCondition +- ValueNotContainsCondition +- ValueEmptyCondition + +#### ValueEqualsCondition + +Properties: + +| Property | Required | Default Value | Description | +|:---------------|:---------|:--------------|:------------------------------------------------------------------------------------------------| +| parameter_name | yes | - | The name of the group that the current group is dependent upon. for example, `page2.isbreaking` | +| value_equals | yes | - | The value the target parameter must equal for this condition to be considered true. | + +#### ValueNotEqualsCondition + +Properties: + +| Property | Required | Default Value | Description | +|:-----------------|:---------|:--------------|:----------------------------------------------------------------------------------------| +| parameter_name | yes | - | The name of the group that the current group is dependent upon. | +| value_not_equals | yes | - | The value the target parameter must not equal for this condition to be considered true. | + +#### ValueContainsCondition + +Properties: + +| Property | Required | Default Value | Description | +|:---------------|:---------|:--------------|:------------------------------------------------------------------------------------| +| parameter_name | yes | - | The name of the group that the current group is dependent upon. | +| value_contains | yes | - | A value the target parameter must contain for this condition to be considered true. | + +#### ValueNotContainsCondition + +Properties: + +| Property | Required | Default Value | Description | +|:-------------------|:---------|:--------------|:----------------------------------------------------------------------------------------| +| parameter_name | yes | - | The name of the group that the current group is dependent upon. | +| value_not_contains | yes | - | A value the target parameter must not contain for this condition to be considered true. | + +#### ValueEmptyCondition + +Properties: + +| Property | Required | Default Value | Description | +|:---------------|:---------|:--------------|:---------------------------------------------------------------------------------| +| parameter_name | yes | - | The name of the group that the current group is dependent upon. | +| value_empty | yes | - | A bool value reflecting whether the expected parameter should be empty or not. | + ### Multiple Templates -You can define multiple templates in the `.git-czrc` file, separated by `---`: +You can define multiple templates in the `.czrc` file, separated by `---`: ```yaml name: angular-template items: - - name: scope - desc: "Scope. Could be anything specifying place of the commit change:" - type: input - # ... +# ... format: "{{.type}}{{with .scope}}({{.}}){{end}}: {{.subject}}{{with .body}}\n\n{{.}}{{end}}{{with .footer}}\n\n{{.}}{{end}}"` --- name: my-template items: - - name: scope - desc: "Scope. Could be anything specifying place of the commit change:" - type: input - # ... +# ... format: "{{.type}}{{with .scope}}({{.}}){{end}}: {{.subject}}{{with .body}}\n\n{{.}}{{end}}{{with .footer}}\n\n{{.}}{{end}}"` ``` -![multiple-templates](https://github.com/shipengqi/illustrations/blob/ebe8786a60c6467edb3122723d74d22f639fb216/commitizen/multiple-templates.png?raw=true) \ No newline at end of file +![multiple-templates](https://github.com/shipengqi/illustrations/blob/e0d588dd70551344f0394cbf6671b15ae22e7635/commitizen/multiple-templates.png?raw=true) diff --git a/cmd/cz/cz.go b/cmd/cz/cz.go index 383defa..4119463 100644 --- a/cmd/cz/cz.go +++ b/cmd/cz/cz.go @@ -3,10 +3,15 @@ package cz import ( "errors" "fmt" + "os" + "path/filepath" + "time" cliflag "github.com/shipengqi/component-base/cli/flag" "github.com/shipengqi/component-base/term" "github.com/shipengqi/golib/convutil" + "github.com/shipengqi/golib/fsutil" + "github.com/shipengqi/log" "github.com/spf13/cobra" "github.com/shipengqi/commitizen/internal/config" @@ -19,6 +24,26 @@ func New() *cobra.Command { c := &cobra.Command{ Use: "commitizen", Long: `Command line utility to standardize git commit messages.`, + PreRun: func(_ *cobra.Command, _ []string) { + if !o.Debug { + return + } + opts := &log.Options{ + DisableRotate: true, + DisableFileCaller: true, + DisableConsoleCaller: true, + DisableConsoleLevel: true, + DisableConsoleTime: true, + Output: filepath.Join(os.TempDir(), "commitizen/logs"), + FileLevel: log.DebugLevel.String(), + FilenameEncoder: filenameEncoder, + } + err := fsutil.MkDirAll(opts.Output) + if err != nil { + panic(err) + } + log.Configure(opts) + }, RunE: func(_ *cobra.Command, _ []string) error { isRepo, err := git.IsGitRepo() if err != nil { @@ -34,7 +59,7 @@ func New() *cobra.Command { return err } - msg, err := tmpl.Run(o.NoTTY) + msg, err := tmpl.Run() if err != nil { return err } @@ -80,3 +105,7 @@ func New() *cobra.Command { return c } + +func filenameEncoder() string { + return fmt.Sprintf("%s.%s.log", filepath.Base(os.Args[0]), time.Now().Format("20060102150405")) +} diff --git a/go.mod b/go.mod index cc68a2d..a3e44a0 100644 --- a/go.mod +++ b/go.mod @@ -1,49 +1,66 @@ module github.com/shipengqi/commitizen -go 1.22 +go 1.24 require ( - github.com/charmbracelet/bubbles v0.18.0 - github.com/charmbracelet/bubbletea v0.25.0 - github.com/charmbracelet/lipgloss v0.10.0 - github.com/onsi/ginkgo/v2 v2.17.1 - github.com/onsi/gomega v1.33.0 - github.com/shipengqi/component-base v0.2.9 - github.com/shipengqi/golib v0.2.12 - github.com/spf13/cobra v1.8.0 - github.com/spf13/pflag v1.0.5 + github.com/charmbracelet/huh v0.7.0 + github.com/mitchellh/mapstructure v1.5.0 + github.com/onsi/ginkgo/v2 v2.23.4 + github.com/onsi/gomega v1.38.0 + github.com/shipengqi/component-base v0.2.11 + github.com/shipengqi/golib v0.2.27 + github.com/shipengqi/log v0.2.3 + github.com/spf13/cobra v1.9.1 + github.com/spf13/pflag v1.0.7 + github.com/stretchr/testify v1.10.0 gopkg.in/yaml.v3 v3.0.1 ) +replace golang.org/x/net v0.35.0 => golang.org/x/net v0.37.0 + require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect - github.com/fatih/color v1.16.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect - github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect + github.com/catppuccin/go v0.3.0 // indirect + github.com/charmbracelet/bubbles v0.21.0 // indirect + github.com/charmbracelet/bubbletea v1.3.4 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.8.0 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/kr/text v0.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/moby/term v0.5.0 // indirect - github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.15.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect - golang.org/x/net v0.23.0 // indirect - golang.org/x/sync v0.6.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/term v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.17.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + go.uber.org/automaxprocs v1.6.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/tools v0.33.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect ) diff --git a/go.sum b/go.sum index 47bf994..92fa63a 100644 --- a/go.sum +++ b/go.sum @@ -1,48 +1,74 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 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/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= -github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= -github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= -github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= -github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= -github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= -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/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= +github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= +github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= +github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= +github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc= +github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= +github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= +github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 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/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -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/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 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/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -52,66 +78,89 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -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-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= -github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= -github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -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/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8= -github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= -github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE= -github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= +github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= +github.com/onsi/gomega v1.38.0 h1:c/WX+w8SLAinvuKKQFh77WEucCnPk4j2OTUr7lt7BeY= +github.com/onsi/gomega v1.38.0/go.mod h1:OcXcwId0b9QsE7Y49u+BTrL4IdKOBOKnD6VQNTJEB6o= 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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y= -github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -github.com/shipengqi/component-base v0.2.9 h1:4XRB6PzTRgqKkkxkJwnpK8YOqDHRzXviyyis67YBhgE= -github.com/shipengqi/component-base v0.2.9/go.mod h1:LfbMJtgUW7nNPwmVIi5wJMif/066edkcIJtkDDJgEQQ= -github.com/shipengqi/golib v0.2.12 h1:/0hrev7+J8KChxEvoVdS2kbGQT8VO4C4qFAhtn6ZI8o= -github.com/shipengqi/golib v0.2.12/go.mod h1:PIezev9VXxmhjawpu3j1JgLSNKLMq5AB8gLouJ83mrw= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/shipengqi/component-base v0.2.11 h1:oNCwa3FhFBGtKVJHSc3Tr+h9ERMX6JAVxkKToeIIh4M= +github.com/shipengqi/component-base v0.2.11/go.mod h1:YJoIETZyVgILBSqiA7ze+WXgh8j/qz8WN+gpiTfnhc8= +github.com/shipengqi/golib v0.2.27 h1:X9Rq5XVaJZhVxiIj2N8Gfkwxhwa1G/u4uA+tWmMYVBY= +github.com/shipengqi/golib v0.2.27/go.mod h1:qFS6uEuKbQxVmcFpygC/PsMXphIjOx/51Y8xA2pL4QM= +github.com/shipengqi/log v0.2.3 h1:dH1LEgFV1jkojNvVf2qdjcYrWIEQ7LsHPkPaeefsTcA= +github.com/shipengqi/log v0.2.3/go.mod h1:YqXfNjg7aDR/KrXoU5KC3vCQ/YldJltQbyEwnlpJOb4= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +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/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -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/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hack/include/go.mk b/hack/include/go.mk index 13524b8..992a71d 100644 --- a/hack/include/go.mk +++ b/hack/include/go.mk @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -GO_SUPPORTED_VERSIONS ?= 1.18|1.19|1.20|1.21|1.22 +GO_SUPPORTED_VERSIONS ?= 1.20|1.21|1.22|1.23|1.24 .PHONY: go.build.verify go.build.verify: diff --git a/hack/include/test.mk b/hack/include/test.mk index e6dbd4b..04564ea 100644 --- a/hack/include/test.mk +++ b/hack/include/test.mk @@ -14,7 +14,6 @@ GINKGO := $(shell go env GOPATH)/bin/ginkgo CLI ?= $(OUTPUT_DIR)/commitizen -NO_TTY ?= 0 .PHONY: test.cover test.cover: @@ -25,4 +24,4 @@ test.cover: .PHONY: test.e2e test.e2e: tools.verify.ginkgo @echo "===========> Run e2e test, CLI: $(CLI)" - @$(GINKGO) -v $(REPO_ROOT)/test/e2e -- -cli=$(CLI) -no-tty=$(NO_TTY) + @$(GINKGO) -v $(REPO_ROOT)/test/e2e -- -cli=$(CLI) diff --git a/internal/config/config.go b/internal/config/config.go index 00edbf3..bd581ba 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -8,45 +8,61 @@ import ( "os" "path/filepath" + "github.com/charmbracelet/huh" "github.com/shipengqi/golib/convutil" "github.com/shipengqi/golib/fsutil" "github.com/shipengqi/golib/sysutil" "gopkg.in/yaml.v3" "github.com/shipengqi/commitizen/internal/options" - "github.com/shipengqi/commitizen/internal/render" - "github.com/shipengqi/commitizen/internal/ui" + "github.com/shipengqi/commitizen/internal/templates" ) const ( - RCFilename = ".git-czrc" - ReservedDefaultName = "default" + RCFilename = ".czrc" + ReservedDefaultName = "default" + FieldKeyTemplateSelect = "template-select" ) +// Config represents a configuration object. type Config struct { - defaultTmpl *render.Template - more []*render.Template + defaultTmpl *templates.Template + more []*templates.Template } +// New creates a new Config object. func New() *Config { return &Config{} } func (c *Config) initialize() error { var fpath string + // Check if the configuration file is local to the repo. if fsutil.IsExists(RCFilename) { fpath = RCFilename } else { + // Check if the configuration file is in the user's home directory. home := sysutil.HomeDir() p := filepath.Join(home, RCFilename) if fsutil.IsExists(p) { fpath = p + } else { + // Check if the configuration file is in the configured + // XDG_CONFIG_HOME directory. + xdgConfigHome, found := os.LookupEnv("XDG_CONFIG_HOME") + if !found { + xdgConfigHome = sysutil.HomeDir() + } + xdgConfigPath := filepath.Join(xdgConfigHome, "commitizen", RCFilename) + if fsutil.IsExists(xdgConfigPath) { + fpath = xdgConfigPath + } } } tmpls, err := LoadTemplates(fpath) if err != nil { - return err + return fmt.Errorf("load templates %s failed: %v", fpath, err) } exists := make(map[string]struct{}, len(tmpls)) for _, v := range tmpls { @@ -59,7 +75,7 @@ func (c *Config) initialize() error { continue } if v.Name == ReservedDefaultName { - return errors.New("template name 'default' is reserved, use 'default' as the template name, default must be true") + return errors.New("template name 'default' is reserved, to override the default template, you need to set default to true") } if _, ok := exists[v.Name]; ok { return fmt.Errorf("duplicate template '%s'", v.Name) @@ -80,7 +96,7 @@ func (c *Config) initialize() error { return nil } -func (c *Config) Run(opts *options.Options) (*render.Template, error) { +func (c *Config) Run(opts *options.Options) (*templates.Template, error) { err := c.initialize() if err != nil { return nil, err @@ -103,14 +119,12 @@ func (c *Config) Run(opts *options.Options) (*render.Template, error) { } if len(c.more) > 0 { - model := c.createTemplatesSelect("Select a template to use for this commit:") - if _, err = ui.Run(model, opts.NoTTY); err != nil { + form := c.createTemplatesSelect("Select the template of change that you're committing:") + if err = form.Run(); err != nil { return nil, err } - if model.Canceled() { - return nil, render.ErrCanceled - } - val := model.Value() + + val := form.GetString(FieldKeyTemplateSelect) if val == c.defaultTmpl.Name { return c.defaultTmpl, nil } @@ -123,28 +137,27 @@ func (c *Config) Run(opts *options.Options) (*render.Template, error) { return c.defaultTmpl, nil } -func (c *Config) createTemplatesSelect(label string) *ui.SelectModel { - var choices ui.Choices - var all []*render.Template +func (c *Config) createTemplatesSelect(label string) *huh.Form { + var choices []string + var all []*templates.Template all = append(all, c.more...) all = append(all, c.defaultTmpl) // list custom templates and default templates for _, v := range all { - choices = append(choices, ui.Choice(v.Name)) - } - height := 8 - if len(all) > 5 { - height = 12 - } else if len(all) > 3 { - height = 10 - } else if len(all) > 2 { - height = 9 + choices = append(choices, v.Name) } - m := ui.NewSelect(label, choices).WithHeight(height) - return m + + return huh.NewForm(huh.NewGroup( + huh.NewNote().Title("Commitizen").Description("Welcome to Commitizen!\nFor further configuration visit:\nhttps://github.com/shipengqi/commitizen"), + huh.NewSelect[string](). + Key(FieldKeyTemplateSelect). + Options(huh.NewOptions(choices...)...). + Title(label)), + ) } -func LoadTemplates(file string) ([]*render.Template, error) { +// LoadTemplates reads a list of templates from the provided file. +func LoadTemplates(file string) ([]*templates.Template, error) { if len(file) == 0 { return nil, nil } @@ -156,15 +169,17 @@ func LoadTemplates(file string) ([]*render.Template, error) { return load(fd) } -func Load(data []byte) ([]*render.Template, error) { +// Load reads a list of templates from the provided byte slice. +func Load(data []byte) ([]*templates.Template, error) { return load(bytes.NewReader(data)) } -func load(reader io.Reader) ([]*render.Template, error) { - var templates []*render.Template +// load reads a list of templates from the provided io.Reader. +func load(reader io.Reader) ([]*templates.Template, error) { + var tmpls []*templates.Template d := yaml.NewDecoder(reader) for { - tmpl := new(render.Template) + tmpl := new(templates.Template) err := d.Decode(tmpl) if err != nil { if errors.Is(err, io.EOF) { @@ -172,8 +187,12 @@ func load(reader io.Reader) ([]*render.Template, error) { } return nil, err } - templates = append(templates, tmpl) + err = tmpl.Initialize() + if err != nil { + return nil, err + } + tmpls = append(tmpls, tmpl) } - return templates, nil + return tmpls, nil } diff --git a/internal/config/default.go b/internal/config/default.go index e8b0de5..d4aa6cc 100644 --- a/internal/config/default.go +++ b/internal/config/default.go @@ -1,44 +1,70 @@ package config +// DefaultCommitTemplate is the default commit template. const DefaultCommitTemplate = `--- name: default default: true +groups: + - name: hasbreaking + depends_on: + and_conditions: + - parameter_name: page2.isbreaking + value_equals: true + - name: nobreaking + depends_on: + and_conditions: + - parameter_name: page2.isbreaking + value_equals: false items: - name: type - desc: "Select the type of change that you're committing:" - type: select + group: page1 + label: "Select the type of change that you're committing:" + type: list options: - - name: feat - desc: "A new feature" - - name: fix - desc: "A bug fix" - - name: docs - desc: "Documentation only changes" - - name: test - desc: "Adding missing tests" - - name: WIP - desc: "Work in progress" - - name: chore - desc: "Changes to the build process or auxiliary tools and\n libraries such as documentation generation" - - name: style - desc: "Changes that do not affect the meaning of the code\n (white-space, formatting, missing semi-colons, etc)" - - name: refactor - desc: "A code change that neither fixes a bug nor adds a feature" - - name: perf - desc: "A code change that improves performance" - - name: revert - desc: "Revert to a commit" + - value: feat + key: "feat: A new feature" + - value: fix + key: "fix: A bug fix" + - value: docs + key: "docs: Documentation only changes" + - value: test + key: "test: Adding missing or correcting existing tests" + - value: chore + key: "chore: Changes to the build process or auxiliary tools and libraries such as documentation generation" + - value: style + key: "style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)" + - value: refactor + key: "refactor: A code change that neither fixes a bug nor adds a feature" + - value: perf + key: "perf: A code change that improves performance" + - value: revert + key: "revert: Reverts a previous commit" - name: scope - desc: "Scope. Could be anything specifying place of the commit change:" - type: input + group: page2 + label: "Scope. What is the scope of this change? (class or file name):" + type: string + trim: true - name: subject - desc: "Subject. Concise description of the changes. Imperative, lower case and no final dot:" - type: input + group: page2 + label: "Subject. Write a short and imperative summary of the code change (lower case and no period):" + type: string required: true - - name: body - desc: "Body. Motivation for the change and contrast this with previous behavior:" - type: textarea + trim: true + - name: isbreaking + group: page2 + label: "Are there any breaking changes?" + type: boolean + - name: hasbreakingbody + group: hasbreaking + label: "A BREAKING CHANGE commit requires a body. Provide additional contextual information about the code changes:" + type: text + required: true + - name: nobreakingbody + group: nobreaking + label: "Body. Provide additional contextual information about the code changes:" + type: text - name: footer - desc: "Footer. Information about Breaking Changes and reference issues that this commit closes:" - type: textarea -format: "{{.type}}{{with .scope}}({{.}}){{end}}: {{.subject}}{{with .body}}\n\n{{.}}{{end}}{{with .footer}}\n\n{{.}}{{end}}"` + group: page3 + label: "Footer. Information about Breaking Changes and reference issues that this commit closes:" + type: text +format: "{{.type}}{{with .scope}}({{.}}){{end}}: {{.subject}}{{with .hasbreakingbody}}\n\n{{.}}{{end}}{{with .nobreakingbody}}\n\n{{.}}{{end}}{{with .footer}}\n\n{{.}}{{end}}"` diff --git a/internal/errors/errors.go b/internal/errors/errors.go new file mode 100644 index 0000000..dbc39bb --- /dev/null +++ b/internal/errors/errors.go @@ -0,0 +1,7 @@ +package errors + +import "errors" + +var ( + ErrType = errors.New("type error") +) diff --git a/internal/errors/missing.go b/internal/errors/missing.go new file mode 100644 index 0000000..4260827 --- /dev/null +++ b/internal/errors/missing.go @@ -0,0 +1,23 @@ +package errors + +import "fmt" + +type MissingErr struct { + name string + field string +} + +func (e MissingErr) Error() string { + if e.name == "" { + return fmt.Sprintf("missing required field `%s`", e.field) + } + return fmt.Sprintf("item '%s' missing required field: %s", e.name, e.field) +} + +func NewMissingErr(field string, name ...string) error { + err := MissingErr{field: field} + if len(name) > 0 { + err.name = name[0] + } + return err +} diff --git a/internal/errors/required.go b/internal/errors/required.go new file mode 100644 index 0000000..96f67a3 --- /dev/null +++ b/internal/errors/required.go @@ -0,0 +1,15 @@ +package errors + +import "fmt" + +type RequiredErr struct { + field string +} + +func (e RequiredErr) Error() string { + return fmt.Sprintf("%s is required", e.field) +} + +func NewRequiredErr(field string) error { + return RequiredErr{field: field} +} diff --git a/internal/git/options.go b/internal/git/options.go index d9e53ba..8a2ab92 100644 --- a/internal/git/options.go +++ b/internal/git/options.go @@ -1,28 +1,36 @@ package git -import "github.com/spf13/pflag" +import ( + "strings" + + "github.com/spf13/pflag" +) type Options struct { - Quiet bool - Verbose bool - SignOff bool - All bool - Amend bool - DryRun bool - Author string - Date string + Quiet bool + Verbose bool + SignOff bool + All bool + Amend bool + DryRun bool + NoVerify bool + Author string + Date string + ExtraGitFlags []string } func NewOptions() *Options { return &Options{ - Quiet: false, - Verbose: false, - SignOff: false, - All: false, - Amend: false, - DryRun: false, - Author: "", - Date: "", + Quiet: false, + Verbose: false, + SignOff: false, + All: false, + Amend: false, + NoVerify: false, + DryRun: false, + Author: "", + Date: "", + ExtraGitFlags: []string{}, } } @@ -35,6 +43,8 @@ func (o *Options) AddFlags(f *pflag.FlagSet) { f.BoolVarP(&o.All, "all", "a", o.All, "commit all changed files.") f.BoolVarP(&o.SignOff, "signoff", "s", o.SignOff, "add a Signed-off-by trailer.") f.BoolVar(&o.Amend, "amend", o.Amend, "amend previous commit") + f.BoolVarP(&o.NoVerify, "no-verify", "n", o.NoVerify, "bypass pre-commit and commit-msg hooks.") + f.StringSliceVar(&o.ExtraGitFlags, "git-flag", o.ExtraGitFlags, "git flags, e.g. --git-flag=\"--branch\"") } func (o *Options) Combine(filename string) []string { @@ -61,9 +71,30 @@ func (o *Options) Combine(filename string) []string { if o.Amend { combination = append(combination, "--amend") } + if o.NoVerify { + combination = append(combination, "--no-verify") + } if o.DryRun { combination = append(combination, "--dry-run") } + if len(o.ExtraGitFlags) > 0 { + result := deDuplicateFlag(o.ExtraGitFlags, "-F", "--file") + combination = append(combination, result...) + } return combination } + +func deDuplicateFlag(sli []string, short, long string) []string { + var result []string + for _, s := range sli { + if strings.HasPrefix(s, short) { + continue + } + if strings.HasPrefix(s, long) { + continue + } + result = append(result, s) + } + return result +} diff --git a/internal/helpers/contains.go b/internal/helpers/contains.go new file mode 100644 index 0000000..025e87b --- /dev/null +++ b/internal/helpers/contains.go @@ -0,0 +1,79 @@ +package helpers + +import ( + "reflect" + "strings" +) + +// Contains asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// helpers.Contains("Hello World", "World") +// helpers.Contains(["Hello", "World"], "World") +// helpers.Contains({"Hello": "World"}, "Hello") +func Contains(s, contains interface{}) bool { + ok, found := containsElement(s, contains) + if !ok { + return false + } + + return found +} + +// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// helpers.NotContains("Hello World", "Earth") +// helpers.NotContains(["Hello", "World"], "Earth") +// helpers.NotContains({"Hello": "World"}, "Earth") +func NotContains(s, contains interface{}) bool { + ok, found := containsElement(s, contains) + if !ok { + return false + } + + return !found +} + +// containsElement try loop over the list check if the list includes the element. +// return (false, false) if impossible. +// return (true, false) if element was not found. +// return (true, true) if element was found. +func containsElement(list interface{}, element interface{}) (ok, found bool) { + + listValue := reflect.ValueOf(list) + listType := reflect.TypeOf(list) + if listType == nil { + return false, false + } + listKind := listType.Kind() + defer func() { + if e := recover(); e != nil { + ok = false + found = false + } + }() + + if listKind == reflect.String { + elementValue := reflect.ValueOf(element) + return true, strings.Contains(listValue.String(), elementValue.String()) + } + + if listKind == reflect.Map { + mapKeys := listValue.MapKeys() + for i := 0; i < len(mapKeys); i++ { + if ObjectsAreEqual(mapKeys[i].Interface(), element) { + return true, true + } + } + return true, false + } + + for i := 0; i < listValue.Len(); i++ { + if ObjectsAreEqual(listValue.Index(i).Interface(), element) { + return true, true + } + } + return true, false + +} diff --git a/internal/helpers/contains_test.go b/internal/helpers/contains_test.go new file mode 100644 index 0000000..6037b17 --- /dev/null +++ b/internal/helpers/contains_test.go @@ -0,0 +1,68 @@ +package helpers + +import ( + "fmt" + "testing" +) + +func TestContainsNotContains(t *testing.T) { + + type A struct { + Name, Value string + } + list := []string{"Foo", "Bar"} + + complexList := []*A{ + {"b", "c"}, + {"d", "e"}, + {"g", "h"}, + {"j", "k"}, + } + simpleMap := map[interface{}]interface{}{"Foo": "Bar"} + var zeroMap map[interface{}]interface{} + + cases := []struct { + expected interface{} + actual interface{} + result bool + }{ + {"Hello World", "Hello", true}, + {"Hello World", "Salut", false}, + {list, "Bar", true}, + {list, "Salut", false}, + {complexList, &A{"g", "h"}, true}, + {complexList, &A{"g", "e"}, false}, + {simpleMap, "Foo", true}, + {simpleMap, "Bar", false}, + {zeroMap, "Bar", false}, + } + + for _, c := range cases { + t.Run(fmt.Sprintf("Contains(%#v, %#v)", c.expected, c.actual), func(t *testing.T) { + res := Contains(c.expected, c.actual) + + if res != c.result { + if res { + t.Errorf("Contains(%#v, %#v) should return true:\n\t%#v contains %#v", c.expected, c.actual, c.expected, c.actual) + } else { + t.Errorf("Contains(%#v, %#v) should return false:\n\t%#v does not contain %#v", c.expected, c.actual, c.expected, c.actual) + } + } + }) + } + + for _, c := range cases { + t.Run(fmt.Sprintf("NotContains(%#v, %#v)", c.expected, c.actual), func(t *testing.T) { + res := NotContains(c.expected, c.actual) + + // NotContains should be inverse of Contains. If it's not, something is wrong + if res == Contains(c.expected, c.actual) { + if res { + t.Errorf("NotContains(%#v, %#v) should return true:\n\t%#v does not contains %#v", c.expected, c.actual, c.expected, c.actual) + } else { + t.Errorf("NotContains(%#v, %#v) should return false:\n\t%#v contains %#v", c.expected, c.actual, c.expected, c.actual) + } + } + }) + } +} diff --git a/internal/helpers/empty.go b/internal/helpers/empty.go new file mode 100644 index 0000000..2c56686 --- /dev/null +++ b/internal/helpers/empty.go @@ -0,0 +1,50 @@ +package helpers + +import "reflect" + +// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// helpers.Empty(obj) +func Empty(object interface{}) bool { + return isEmpty(object) +} + +// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if helpers.NotEmpty(obj) { +// helpers.Equal("two", obj[1]) +// } +func NotEmpty(object interface{}) bool { + return !isEmpty(object) +} + +// isEmpty gets whether the specified object is considered empty or not. +func isEmpty(object interface{}) bool { + + // get nil case out of the way + if object == nil { + return true + } + + objValue := reflect.ValueOf(object) + + switch objValue.Kind() { + // collection types are empty when they have no element + case reflect.Chan, reflect.Map, reflect.Slice: + return objValue.Len() == 0 + // pointers are empty if nil or if the value they point to is empty + case reflect.Ptr: + if objValue.IsNil() { + return true + } + deref := objValue.Elem().Interface() + return isEmpty(deref) + // for all other types, compare against the zero value + // array types are empty when they match their zero-initialized state + default: + zero := reflect.Zero(objValue.Type()) + return reflect.DeepEqual(object, zero.Interface()) + } +} diff --git a/internal/helpers/empty_test.go b/internal/helpers/empty_test.go new file mode 100644 index 0000000..b42c422 --- /dev/null +++ b/internal/helpers/empty_test.go @@ -0,0 +1,75 @@ +package helpers + +import ( + "errors" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestEmpty(t *testing.T) { + + chWithValue := make(chan struct{}, 1) + chWithValue <- struct{}{} + var tiP *time.Time + var tiNP time.Time + var s *string + var f *os.File + sP := &s + x := 1 + xP := &x + + type TString string + type TStruct struct { + x int + } + + assert.True(t, Empty(""), "Empty string is empty") + assert.True(t, Empty(nil), "Nil is empty") + assert.True(t, Empty([]string{}), "Empty string array is empty") + assert.True(t, Empty(0), "Zero int value is empty") + assert.True(t, Empty(false), "False value is empty") + assert.True(t, Empty(make(chan struct{})), "Channel without values is empty") + assert.True(t, Empty(s), "Nil string pointer is empty") + assert.True(t, Empty(f), "Nil os.File pointer is empty") + assert.True(t, Empty(tiP), "Nil time.Time pointer is empty") + assert.True(t, Empty(tiNP), "time.Time is empty") + assert.True(t, Empty(TStruct{}), "struct with zero values is empty") + assert.True(t, Empty(TString("")), "empty aliased string is empty") + assert.True(t, Empty(sP), "ptr to nil value is empty") + assert.True(t, Empty([1]int{}), "array is state") + + assert.False(t, Empty("something"), "Non Empty string is not empty") + assert.False(t, Empty(errors.New("something")), "Non nil object is not empty") + assert.False(t, Empty([]string{"something"}), "Non empty string array is not empty") + assert.False(t, Empty(1), "Non-zero int value is not empty") + assert.False(t, Empty(true), "True value is not empty") + assert.False(t, Empty(chWithValue), "Channel with values is not empty") + assert.False(t, Empty(TStruct{x: 1}), "struct with initialized values is empty") + assert.False(t, Empty(TString("abc")), "non-empty aliased string is empty") + assert.False(t, Empty(xP), "ptr to non-nil value is not empty") + assert.False(t, Empty([1]int{42}), "array is not state") +} + +func TestNotEmpty(t *testing.T) { + chWithValue := make(chan struct{}, 1) + chWithValue <- struct{}{} + + assert.False(t, NotEmpty(""), "Empty string is empty") + assert.False(t, NotEmpty(nil), "Nil is empty") + assert.False(t, NotEmpty([]string{}), "Empty string array is empty") + assert.False(t, NotEmpty(0), "Zero int value is empty") + assert.False(t, NotEmpty(false), "False value is empty") + assert.False(t, NotEmpty(make(chan struct{})), "Channel without values is empty") + assert.False(t, NotEmpty([1]int{}), "array is state") + + assert.True(t, NotEmpty("something"), "Non Empty string is not empty") + assert.True(t, NotEmpty(errors.New("something")), "Non nil object is not empty") + assert.True(t, NotEmpty([]string{"something"}), "Non empty string array is not empty") + assert.True(t, NotEmpty(1), "Non-zero int value is not empty") + assert.True(t, NotEmpty(true), "True value is not empty") + assert.True(t, NotEmpty(chWithValue), "Channel with values is not empty") + assert.True(t, NotEmpty([1]int{42}), "array is not state") +} diff --git a/internal/helpers/equal.go b/internal/helpers/equal.go new file mode 100644 index 0000000..28cde77 --- /dev/null +++ b/internal/helpers/equal.go @@ -0,0 +1,80 @@ +package helpers + +import ( + "bytes" + "errors" + "reflect" +) + +// Equal asserts that two objects are equal. +// +// helpers.Equal(123, 123) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func Equal(expected, actual interface{}) bool { + if err := validateEqualArgs(expected, actual); err != nil { + // invalid operation + return false + } + return ObjectsAreEqual(expected, actual) +} + +// NotEqual asserts that the specified values are NOT equal. +// +// helpers.NotEqual(obj1, obj2) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func NotEqual(expected, actual interface{}) bool { + if err := validateEqualArgs(expected, actual); err != nil { + // invalid operation + return false + } + + return !ObjectsAreEqual(expected, actual) +} + +// ObjectsAreEqual determines if two objects are considered equal. +// +// This function does no assertion of any kind. +func ObjectsAreEqual(expected, actual interface{}) bool { + if expected == nil || actual == nil { + return expected == actual + } + + exp, ok := expected.([]byte) + if !ok { + return reflect.DeepEqual(expected, actual) + } + + act, ok := actual.([]byte) + if !ok { + return false + } + if exp == nil || act == nil { + return exp == nil && act == nil + } + return bytes.Equal(exp, act) +} + +// validateEqualArgs checks whether provided arguments can be safely used in the +// Equal/NotEqual functions. +func validateEqualArgs(expected, actual interface{}) error { + if expected == nil && actual == nil { + return nil + } + + if isFunction(expected) || isFunction(actual) { + return errors.New("cannot take func type as argument") + } + return nil +} + +func isFunction(arg interface{}) bool { + if arg == nil { + return false + } + return reflect.TypeOf(arg).Kind() == reflect.Func +} diff --git a/internal/helpers/equal_test.go b/internal/helpers/equal_test.go new file mode 100644 index 0000000..ad8c3da --- /dev/null +++ b/internal/helpers/equal_test.go @@ -0,0 +1,92 @@ +package helpers + +import ( + "fmt" + "testing" +) + +func TestEqual(t *testing.T) { + type myType string + + var m map[string]interface{} + + cases := []struct { + expected interface{} + actual interface{} + result bool + remark string + }{ + {"Hello World", "Hello World", true, ""}, + {123, 123, true, ""}, + {123.5, 123.5, true, ""}, + {[]byte("Hello World"), []byte("Hello World"), true, ""}, + {nil, nil, true, ""}, + {int32(123), int32(123), true, ""}, + {uint64(123), uint64(123), true, ""}, + {myType("1"), myType("1"), true, ""}, + {&struct{}{}, &struct{}{}, true, "pointer equality is based on equality of underlying value"}, + {[]string{"str1", "str2"}, []string{"str1", "str2"}, true, ""}, + {[]int{1, 2}, []int{1, 2}, true, ""}, + {true, true, true, ""}, + + // Not expected to be equal + {m["bar"], "something", false, ""}, + {myType("1"), myType("2"), false, ""}, + + // A case that might be confusing, especially with numeric literals + {10, uint(10), false, ""}, + + {[]string{"str1", "str2"}, []string{"str1", "str3"}, false, ""}, + {[]int{1, 2}, []int{1, 3}, false, ""}, + {true, false, false, ""}, + } + + for _, c := range cases { + t.Run(fmt.Sprintf("Equal(%#v, %#v)", c.expected, c.actual), func(t *testing.T) { + res := Equal(c.expected, c.actual) + + if res != c.result { + t.Errorf("Equal(%#v, %#v) should return %#v: %s", c.expected, c.actual, c.result, c.remark) + } + }) + } +} + +func TestNotEqual(t *testing.T) { + type myStructType struct{} + + cases := []struct { + expected interface{} + actual interface{} + result bool + }{ + // cases that are expected not to match + {"Hello World", "Hello World!", true}, + {123, 1234, true}, + {123.5, 123.55, true}, + {[]byte("Hello World"), []byte("Hello World!"), true}, + {nil, new(myStructType), true}, + + // cases that are expected to match + {nil, nil, false}, + {"Hello World", "Hello World", false}, + {123, 123, false}, + {123.5, 123.5, false}, + {[]byte("Hello World"), []byte("Hello World"), false}, + {new(myStructType), new(myStructType), false}, + {&struct{}{}, &struct{}{}, false}, + {func() int { return 23 }, func() int { return 24 }, false}, + // A case that might be confusing, especially with numeric literals + {int(10), uint(10), true}, + } + + for _, c := range cases { + t.Run(fmt.Sprintf("NotEqual(%#v, %#v)", c.expected, c.actual), func(t *testing.T) { + res := NotEqual(c.expected, c.actual) + + if res != c.result { + t.Errorf("NotEqual(%#v, %#v) should return %#v", c.expected, c.actual, c.result) + } + }) + } +} diff --git a/internal/helpers/helper.go b/internal/helpers/helper.go new file mode 100644 index 0000000..589b2f5 --- /dev/null +++ b/internal/helpers/helper.go @@ -0,0 +1,21 @@ +package helpers + +import "github.com/shipengqi/commitizen/internal/errors" + +func GetValueFromYAML[T any](data map[string]interface{}, key string) (T, error) { + var ( + res T + ok bool + v interface{} + ) + + v, ok = data[key] + if !ok { + return res, errors.NewMissingErr(key) + } + res, ok = v.(T) + if !ok { + return res, errors.ErrType + } + return res, nil +} diff --git a/internal/options/options.go b/internal/options/options.go index c15e075..734df43 100644 --- a/internal/options/options.go +++ b/internal/options/options.go @@ -1,6 +1,10 @@ package options import ( + "fmt" + "os" + "path/filepath" + cliflag "github.com/shipengqi/component-base/cli/flag" "github.com/shipengqi/commitizen/internal/git" @@ -8,8 +12,8 @@ import ( type Options struct { DryRun bool - NoTTY bool Default bool + Debug bool Template string GitOptions *git.Options } @@ -22,15 +26,16 @@ func New() *Options { } func (o *Options) Flags() (fss cliflag.NamedFlagSets) { + dir := filepath.Join(os.TempDir(), "commitizen/logs") + o.GitOptions.AddFlags(fss.FlagSet("Git Commit")) fs := fss.FlagSet("Commitizen") fs.BoolVar(&o.DryRun, "dry-run", o.DryRun, "do not create a commit, but show the message and list of paths \nthat are to be committed.") fs.StringVarP(&o.Template, "template", "t", o.Template, "template name to use when multiple templates exist.") fs.BoolVarP(&o.Default, "default", "d", o.Default, "use the default template, '--default' has a higher priority than '--template'.") - fs.BoolVar(&o.NoTTY, "no-tty", o.NoTTY, "make sure that the TTY (terminal) is never used for any output.") - - _ = fs.MarkHidden("no-tty") + fs.BoolVar(&o.Debug, "debug", o.Debug, fmt.Sprintf("enable debug mode, writing log file to the %s directory.", dir)) + _ = fs.MarkHidden("debug") return } diff --git a/internal/parameter/boolean/bool.go b/internal/parameter/boolean/bool.go new file mode 100644 index 0000000..2bde03f --- /dev/null +++ b/internal/parameter/boolean/bool.go @@ -0,0 +1,26 @@ +package boolean + +import ( + "github.com/charmbracelet/huh" + + "github.com/shipengqi/commitizen/internal/parameter" +) + +type Param struct { + parameter.Parameter `mapstructure:",squash"` + + DefaultValue bool `yaml:"default_value" json:"default_value" mapstructure:"default_value"` +} + +func (p *Param) Render() { + param := huh.NewConfirm().Key(p.Name). + Title(p.Label) + + if len(p.Description) > 0 { + param.Description(p.Description) + } + + param.Value(&p.DefaultValue) + + p.Field = param +} diff --git a/internal/parameter/group.go b/internal/parameter/group.go new file mode 100644 index 0000000..c8e52a4 --- /dev/null +++ b/internal/parameter/group.go @@ -0,0 +1,179 @@ +package parameter + +import ( + standarderrs "errors" + "fmt" + + "github.com/charmbracelet/huh" + "github.com/shipengqi/golib/strutil" + "github.com/shipengqi/log" + + "github.com/shipengqi/commitizen/internal/errors" + "github.com/shipengqi/commitizen/internal/helpers" +) + +type Group struct { + Name string `yaml:"name" json:"name" mapstructure:"name"` + DependsOn DependsOn `yaml:"depends_on" json:"depends_on" mapstructure:"depends_on"` +} + +func NewGroup(name string) *Group { + return &Group{Name: name} +} + +func (g *Group) Validate() []error { + var errs []error + if strutil.IsEmpty(g.Name) { + errs = append(errs, standarderrs.New("the group missing required field: name")) + } + if !regexName.MatchString(g.Name) { + errs = append(errs, fmt.Errorf("group.name '%s' must match the regex: ^[a-zA-Z0-9-_]{1,62}$", g.Name)) + } + for _, v := range g.DependsOn.OrConditions { + errs = append(errs, v.Validate()...) + } + for _, v := range g.DependsOn.AndConditions { + errs = append(errs, v.Validate()...) + } + return errs +} + +func (g *Group) Render(all map[FieldKey]huh.Field, fields []huh.Field) *huh.Group { + group := huh.NewGroup(fields...) + if len(g.DependsOn.OrConditions) < 1 && len(g.DependsOn.AndConditions) < 1 { + return group + } + + for _, v := range g.DependsOn.OrConditions { + v.fields = all + } + for _, v := range g.DependsOn.AndConditions { + v.fields = all + } + + group.WithHideFunc(func() bool { + orCount := len(g.DependsOn.OrConditions) + andCount := len(g.DependsOn.AndConditions) + + log.Debugf("OrConditions: %d, AndConditions: %d", orCount, andCount) + if orCount < 1 && andCount < 1 { + return false + } + + orMet := false + for _, condition := range g.DependsOn.OrConditions { + if condition.Match() { + orMet = true + break + } + } + + andMetCount := 0 + for _, condition := range g.DependsOn.AndConditions { + if condition.Match() { + andMetCount++ + } + } + log.Debugf("orMet: %v, andMetCount: %d", orMet, andMetCount) + if orCount > 0 && andCount < 1 { + return !orMet + } + if orCount < 1 && andCount > 0 { + return andCount != andMetCount + } + if orCount > 0 && andCount > 0 { + return !orMet || andMetCount != orCount + } + return false + }) + + return group +} + +type DependsOn struct { + AndConditions []*Condition `yaml:"and_conditions" json:"and_conditions" mapstructure:"and_conditions"` + OrConditions []*Condition `yaml:"or_conditions" json:"or_conditions" mapstructure:"or_conditions"` +} + +type Condition struct { + fields map[FieldKey]huh.Field + + ParameterName string `yaml:"parameter_name" json:"parameter_name" mapstructure:"parameter_name"` + ValueEmpty *bool `yaml:"value_empty" json:"value_empty" mapstructure:"value_empty"` + ValueEquals interface{} `yaml:"value_equals" json:"value_equals" mapstructure:"value_equals"` + ValueNotEquals interface{} `yaml:"value_not_equals" json:"value_not_equals" mapstructure:"value_not_equals"` + ValueContains interface{} `yaml:"value_contains" json:"value_contains" mapstructure:"value_contains"` + ValueNotContains interface{} `yaml:"value_not_contains" json:"value_not_contains" mapstructure:"value_not_contains"` +} + +func (c *Condition) Validate() []error { + var errs []error + if strutil.IsEmpty(c.ParameterName) { + errs = append(errs, errors.NewMissingErr("parameter_name", "condition")) + } + if c.ValueEmpty == nil && c.ValueEquals == nil && c.ValueNotEquals == nil && + c.ValueNotContains == nil && c.ValueContains == nil { + errs = append(errs, standarderrs.New("missing a valid condition")) + } + return errs +} + +func (c *Condition) Match() bool { + key := GetFiledKey(c.ParameterName) + log.Debugf("math field condition: %s", key) + field, ok := c.fields[key] + if !ok { + log.Debugf("cannot find field condition: %s", key) + return false + } + val := field.GetValue() + if c.ValueEmpty != nil { + return c.IsEmpty(*c.ValueEmpty, val) + } + if c.ValueEquals != nil { + return c.Equal(val) + } + if c.ValueNotEquals != nil { + return c.NotEqual(val) + } + if c.ValueContains != nil { + return c.Contains(val) + } + if c.ValueNotContains != nil { + log.Debugf("value not contains val: %v", val) + return c.NotContains(val) + } + return false +} + +func (c *Condition) Equal(val interface{}) bool { + log.Debugf("%v contains match val: %v", c.ValueEquals, val) + return helpers.Equal(c.ValueEquals, val) +} + +func (c *Condition) NotEqual(val interface{}) bool { + log.Debugf("%v not equals val: %v", c.ValueNotEquals, val) + return helpers.NotEqual(c.ValueNotEquals, val) +} + +func (c *Condition) Contains(val interface{}) bool { + log.Debugf("%v contains val: %v", val, c.ValueContains) + return helpers.Contains(val, c.ValueContains) +} + +func (c *Condition) NotContains(val interface{}) bool { + log.Debugf("%v not contains val: %v", val, c.ValueContains) + return helpers.NotContains(val, c.ValueNotContains) +} + +func (c *Condition) IsEmpty(empty bool, val interface{}) bool { + if empty && helpers.Empty(val) { + log.Debugf("value is empty: %v", val) + return true + } + if !empty && helpers.NotEmpty(val) { + log.Debugf("value is not empty: %v", val) + return true + } + return false +} diff --git a/internal/parameter/integer/int.go b/internal/parameter/integer/int.go new file mode 100644 index 0000000..fd0b356 --- /dev/null +++ b/internal/parameter/integer/int.go @@ -0,0 +1,44 @@ +package integer + +import ( + "github.com/charmbracelet/huh" + + "github.com/shipengqi/commitizen/internal/parameter" + "github.com/shipengqi/commitizen/internal/parameter/validators" +) + +type Param struct { + parameter.Parameter `mapstructure:",squash"` + + DefaultValue string `yaml:"default_value" json:"default_value" mapstructure:"default_value"` + Required bool `yaml:"required" json:"required" mapstructure:"required"` + Min *int `yaml:"min" json:"min" mapstructure:"min"` + Max *int `yaml:"max" json:"max" mapstructure:"max"` +} + +func (p *Param) Render() { + param := huh.NewInput().Key(p.Name). + Title(p.Label) + + if len(p.Description) > 0 { + param.Description(p.Description) + } + + param.Value(&p.DefaultValue) + + var group []validators.Validator[string] + if p.Required { + group = append(group, validators.Required(p.Name, true)) + } + if p.Min != nil { + group = append(group, validators.Min(*p.Min)) + } + if p.Max != nil { + group = append(group, validators.Max(*p.Max)) + } + + if len(group) > 0 { + param.Validate(validators.Group(group...)) + } + p.Field = param +} diff --git a/internal/parameter/list/list.go b/internal/parameter/list/list.go new file mode 100644 index 0000000..68c4a11 --- /dev/null +++ b/internal/parameter/list/list.go @@ -0,0 +1,49 @@ +package list + +import ( + "github.com/charmbracelet/huh" + + "github.com/shipengqi/commitizen/internal/errors" + "github.com/shipengqi/commitizen/internal/parameter" + "github.com/shipengqi/commitizen/internal/parameter/validators" +) + +type Param struct { + parameter.Parameter `mapstructure:",squash"` + + Options []huh.Option[string] `yaml:"options" json:"options" mapstructure:"options"` + Height *int `yaml:"height" json:"height" mapstructure:"height"` + DefaultValue string `yaml:"default_value" json:"default_value" mapstructure:"default_value"` + Required bool `yaml:"required" json:"required" mapstructure:"required"` +} + +func (p *Param) Validate() []error { + errs := p.Parameter.Validate() + if len(p.Options) < 1 { + errs = append(errs, errors.NewMissingErr("options", p.Name)) + } + return errs +} + +func (p *Param) Render() { + param := huh.NewSelect[string]().Key(p.Name). + Options(p.Options...). + Title(p.Label) + if len(p.Description) > 0 { + param.Description(p.Description) + } + if p.Height != nil { + param.Height(*p.Height) + } + param.Value(&p.DefaultValue) + + var group []validators.Validator[string] + if p.Required { + group = append(group, validators.Required(p.Name, false)) + } + + if len(group) > 0 { + param.Validate(validators.Group(group...)) + } + p.Field = param +} diff --git a/internal/parameter/multilist/list.go b/internal/parameter/multilist/list.go new file mode 100644 index 0000000..8fb0e47 --- /dev/null +++ b/internal/parameter/multilist/list.go @@ -0,0 +1,54 @@ +package multilist + +import ( + "github.com/charmbracelet/huh" + + "github.com/shipengqi/commitizen/internal/errors" + "github.com/shipengqi/commitizen/internal/parameter" + "github.com/shipengqi/commitizen/internal/parameter/validators" +) + +type Param struct { + parameter.Parameter `mapstructure:",squash"` + + Options []huh.Option[string] `yaml:"options" json:"options" mapstructure:"options"` + DefaultValue []string `yaml:"default_value" json:"default_value" mapstructure:"default_value"` + Required bool `yaml:"required" json:"required" mapstructure:"required"` + Limit *int `yaml:"limit" json:"limit" mapstructure:"limit"` + Height *int `yaml:"height" json:"height" mapstructure:"height"` +} + +func (p *Param) Validate() []error { + errs := p.Parameter.Validate() + if len(p.Options) < 1 { + errs = append(errs, errors.NewMissingErr("options", p.Name)) + } + return errs +} + +func (p *Param) Render() { + param := huh.NewMultiSelect[string]().Key(p.Name). + Options(p.Options...). + Title(p.Label) + if len(p.Description) > 0 { + param.Description(p.Description) + } + if p.Height != nil { + param.Height(*p.Height) + } + if p.Limit != nil { + param.Limit(*p.Limit) + } + param.Value(&p.DefaultValue) + + var group []validators.Validator[[]string] + if p.Required { + group = append(group, validators.MultiRequired(p.Name)) + } + + if p.Required { + param.Validate(validators.Group(group...)) + } + + p.Field = param +} diff --git a/internal/parameter/param.go b/internal/parameter/param.go new file mode 100644 index 0000000..1a1a7cd --- /dev/null +++ b/internal/parameter/param.go @@ -0,0 +1,50 @@ +package parameter + +import ( + "fmt" + + "github.com/charmbracelet/huh" + "github.com/shipengqi/golib/strutil" + + "github.com/shipengqi/commitizen/internal/errors" +) + +type Interface interface { + huh.Field + GetGroup() string + Render() + Validate() []error +} + +type Parameter struct { + huh.Field `mapstructure:"-"` + Name string `yaml:"name" json:"name" mapstructure:"name"` + Group string `yaml:"group" json:"group" mapstructure:"group"` + Label string `yaml:"label" json:"label" mapstructure:"label"` + Description string `yaml:"description" json:"description" mapstructure:"description"` + Type string `yaml:"type" json:"type" mapstructure:"type"` + DependsOn DependsOn `yaml:"depends_on" json:"depends_on" mapstructure:"depends_on"` +} + +func (p *Parameter) GetGroup() string { + return p.Group +} + +func (p *Parameter) Render() {} + +func (p *Parameter) Validate() []error { + var errs []error + if strutil.IsEmpty(p.Name) { + errs = append(errs, errors.NewMissingErr("parameter.name")) + } + if !regexName.MatchString(p.Name) { + errs = append(errs, fmt.Errorf("parameter.name '%s' must match the regex: ^[a-zA-Z0-9-_]{1,62}$", p.Name)) + } + if strutil.IsEmpty(p.Label) { + errs = append(errs, errors.NewMissingErr("label", p.Name)) + } + if strutil.IsEmpty(p.Type) { + errs = append(errs, errors.NewMissingErr("type", p.Name)) + } + return errs +} diff --git a/internal/parameter/secret/secret.go b/internal/parameter/secret/secret.go new file mode 100644 index 0000000..5717d36 --- /dev/null +++ b/internal/parameter/secret/secret.go @@ -0,0 +1,51 @@ +package secret + +import ( + "fmt" + "regexp" + + "github.com/charmbracelet/huh" + + "github.com/shipengqi/commitizen/internal/parameter/str" + "github.com/shipengqi/commitizen/internal/parameter/validators" +) + +type Param struct { + str.Param `mapstructure:",squash"` +} + +func (p *Param) Validate() []error { + errs := p.Parameter.Validate() + if p.Regex != "" { + if _, err := regexp.Compile(p.Regex); err != nil { + errs = append(errs, fmt.Errorf("regex %s compile: %s", p.Regex, err.Error())) + } + } + return errs +} + +func (p *Param) Render() { + param := p.RenderInput() + param.EchoMode(huh.EchoModePassword) + + // reset validators of the secret + var group []validators.Validator[string] + if p.Required { + group = append(group, validators.Required(p.Name, p.Trim)) + } + // if the value is not required and no value has been given, min length validator should be ignored. + if p.Required && p.MinLength != nil { + group = append(group, validators.MinLength(*p.MinLength)) + } + if p.MaxLength != nil { + group = append(group, validators.MaxLength(*p.MaxLength)) + } + if p.Regex != "" { + group = append(group, validators.RegexValidator(p.Regex)) + } + if len(group) > 0 { + param.Validate(validators.Group(group...)) + } + + p.Field = param +} diff --git a/internal/parameter/str/str.go b/internal/parameter/str/str.go new file mode 100644 index 0000000..9047d3f --- /dev/null +++ b/internal/parameter/str/str.go @@ -0,0 +1,87 @@ +package str + +import ( + "fmt" + "regexp" + "strings" + + "github.com/charmbracelet/huh" + + "github.com/shipengqi/commitizen/internal/parameter" + "github.com/shipengqi/commitizen/internal/parameter/validators" +) + +type Param struct { + parameter.Parameter `mapstructure:",squash"` + + Required bool `yaml:"required" json:"required" mapstructure:"required"` + FQDN bool `yaml:"fqdn" json:"fqdn" mapstructure:"fqdn"` + IP bool `yaml:"ip" json:"ip" mapstructure:"ip"` + Trim bool `yaml:"trim" json:"trim" mapstructure:"trim"` + DefaultValue string `yaml:"default_value" json:"default_value" mapstructure:"default_value"` + Regex string `yaml:"regex" json:"regex" mapstructure:"regex"` + MinLength *int `yaml:"min_length" json:"min_length" mapstructure:"min_length"` + MaxLength *int `yaml:"max_length" json:"max_length" mapstructure:"max_length"` +} + +func (p *Param) Validate() []error { + errs := p.Parameter.Validate() + if p.Regex != "" { + if _, err := regexp.Compile(p.Regex); err != nil { + errs = append(errs, fmt.Errorf("regex %s compile: %s", p.Regex, err.Error())) + } + } + return errs +} + +func (p *Param) Render() { + p.Field = p.RenderInput() +} + +func (p *Param) GetValue() any { + if !p.Trim { + return p.Field.GetValue() + } + val := p.Field.GetValue() + if str, ok := val.(string); ok { + return strings.TrimSpace(str) + } + return p.Field.GetValue() +} + +func (p *Param) RenderInput() *huh.Input { + param := huh.NewInput().Key(p.Name). + Title(p.Label) + + if len(p.Description) > 0 { + param.Description(p.Description) + } + + param.Value(&p.DefaultValue) + + var group []validators.Validator[string] + if p.Required { + group = append(group, validators.Required(p.Name, p.Trim)) + } + // if the value is not required and no value has been given, min length validator should be ignored. + if p.Required && p.MinLength != nil { + group = append(group, validators.MinLength(*p.MinLength)) + } + if p.MaxLength != nil { + group = append(group, validators.MaxLength(*p.MaxLength)) + } + if p.IP { + group = append(group, validators.IPValidator()) + } + if p.FQDN { + group = append(group, validators.FQDNValidator()) + } + if p.Regex != "" { + group = append(group, validators.RegexValidator(p.Regex)) + } + + if len(group) > 0 { + param.Validate(validators.Group(group...)) + } + return param +} diff --git a/internal/parameter/text/text.go b/internal/parameter/text/text.go new file mode 100644 index 0000000..b5c6cec --- /dev/null +++ b/internal/parameter/text/text.go @@ -0,0 +1,68 @@ +package text + +import ( + "fmt" + "regexp" + + "github.com/charmbracelet/huh" + + "github.com/shipengqi/commitizen/internal/parameter" + "github.com/shipengqi/commitizen/internal/parameter/validators" +) + +type Param struct { + parameter.Parameter `mapstructure:",squash"` + + Required bool `yaml:"required" json:"required" mapstructure:"required"` + DefaultValue string `yaml:"default_value" json:"default_value" mapstructure:"default_value"` + Regex string `yaml:"regex" json:"regex" mapstructure:"regex"` + MinLength *int `yaml:"min_length" json:"min_length" mapstructure:"min_length"` + MaxLength *int `yaml:"max_length" json:"max_length" mapstructure:"max_length"` + Height *int `yaml:"height" json:"height" mapstructure:"height"` +} + +func (p *Param) Validate() []error { + errs := p.Parameter.Validate() + if p.Regex != "" { + if _, err := regexp.Compile(p.Regex); err != nil { + errs = append(errs, fmt.Errorf("regex %s compile: %s", p.Regex, err.Error())) + } + } + return errs +} + +func (p *Param) Render() { + param := huh.NewText().Key(p.Name). + Title(p.Label) + + if p.Height != nil { + param.Lines(*p.Height) + } + + if len(p.Description) > 0 { + param.Description(p.Description) + } + + param.Value(&p.DefaultValue) + + var group []validators.Validator[string] + if p.Required { + group = append(group, validators.Required(p.Name, false)) + } + // if the value is not required and no value has been given, min length validator should be ignored. + if p.Required && p.MinLength != nil { + group = append(group, validators.MinLength(*p.MinLength)) + } + if p.MaxLength != nil { + group = append(group, validators.MaxLength(*p.MaxLength)) + } + if p.Regex != "" { + group = append(group, validators.RegexValidator(p.Regex)) + } + + if len(group) > 0 { + param.Validate(validators.Group(group...)) + } + + p.Field = param +} diff --git a/internal/parameter/types.go b/internal/parameter/types.go new file mode 100644 index 0000000..6220860 --- /dev/null +++ b/internal/parameter/types.go @@ -0,0 +1,47 @@ +package parameter + +import ( + "regexp" + "strings" + + "github.com/shipengqi/golib/strutil" +) + +var ( + regexName = regexp.MustCompile(`^[a-zA-Z0-9-_]{1,62}$`) +) + +const ( + TypeBoolean = "boolean" + TypeInteger = "integer" + TypeList = "list" + TypeMultiList = "multi_list" + TypeString = "string" + TypeSecret = "secret" + TypeText = "text" +) + +const UnknownGroup = "unknown" + +type FieldKey string + +func NewFiledKey(item string, group ...string) FieldKey { + if len(group) < 1 { + return FieldKey(UnknownGroup + "." + item) + } + if strutil.IsEmpty(group[0]) { + return FieldKey(UnknownGroup + "." + item) + } + return FieldKey(group[0] + "." + item) +} + +func GetFiledKey(key string) FieldKey { + keys := strings.Split(key, ".") + if len(keys) < 1 { + return "" + } + if len(keys) < 2 { + return FieldKey(UnknownGroup + "." + keys[0]) + } + return FieldKey(key) +} diff --git a/internal/parameter/validators/int.go b/internal/parameter/validators/int.go new file mode 100644 index 0000000..9b689e8 --- /dev/null +++ b/internal/parameter/validators/int.go @@ -0,0 +1,33 @@ +package validators + +import ( + "errors" + "fmt" + "strconv" +) + +func Max(maxVal int) func(string) error { + return func(str string) error { + v, err := strconv.Atoi(str) + if err != nil { + return errors.New("invalid integer") + } + if v > maxVal { + return fmt.Errorf("value must less than or equal to %d", maxVal) + } + return nil + } +} + +func Min(minVal int) func(string) error { + return func(str string) error { + v, err := strconv.Atoi(str) + if err != nil { + return errors.New("invalid integer") + } + if v < minVal { + return fmt.Errorf("value must less than or equal to %d", minVal) + } + return nil + } +} diff --git a/internal/parameter/validators/int_test.go b/internal/parameter/validators/int_test.go new file mode 100644 index 0000000..3e4d37f --- /dev/null +++ b/internal/parameter/validators/int_test.go @@ -0,0 +1 @@ +package validators diff --git a/internal/parameter/validators/required.go b/internal/parameter/validators/required.go new file mode 100644 index 0000000..1d9be01 --- /dev/null +++ b/internal/parameter/validators/required.go @@ -0,0 +1,18 @@ +package validators + +import ( + "fmt" + "strings" +) + +func Required(name string, trim bool) func(string) error { + return func(str string) error { + if trim { + str = strings.TrimSpace(str) + } + if len(str) == 0 { + return fmt.Errorf("'%s' cannot be empty", name) + } + return nil + } +} diff --git a/internal/parameter/validators/slice.go b/internal/parameter/validators/slice.go new file mode 100644 index 0000000..9c06e7a --- /dev/null +++ b/internal/parameter/validators/slice.go @@ -0,0 +1,12 @@ +package validators + +import "github.com/shipengqi/commitizen/internal/errors" + +func MultiRequired(name string) func([]string) error { + return func(vals []string) error { + if len(vals) == 0 { + return errors.NewRequiredErr(name) + } + return nil + } +} diff --git a/internal/parameter/validators/str.go b/internal/parameter/validators/str.go new file mode 100644 index 0000000..78bb1a8 --- /dev/null +++ b/internal/parameter/validators/str.go @@ -0,0 +1,96 @@ +package validators + +import ( + "fmt" + "net" + "regexp" +) + +var ( + regexFQDN = regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9-]{0,62})(\.[a-zA-Z0-9][a-zA-Z0-9-]{0,62})*?(\.[a-zA-Z][a-zA-Z0-9]{0,62})\.?$`) +) + +func MaxLength(maxLen int) func(string) error { + return func(str string) error { + if len(str) > maxLen { + return fmt.Errorf("string must be less than or equal to %d characters", maxLen) + } + return nil + } +} + +func MinLength(minLen int) func(string) error { + return func(str string) error { + if len(str) < minLen { + return fmt.Errorf("string must be greater than or equal to %d characters", minLen) + } + return nil + } +} + +func RegexValidator(regex string) func(string) error { + return func(str string) error { + re := regexp.MustCompile(regex) + if !re.MatchString(str) { + return fmt.Errorf("contents must match the regex: %s", regex) + } + return nil + } +} + +func IPv4Validator() func(string) error { + return func(str string) error { + if !isIPv4(str) { + return fmt.Errorf("%s is not a valid IPv4 address", str) + } + return nil + } +} + +func IPv6Validator() func(string) error { + return func(s string) error { + if !isIPv6(s) { + return fmt.Errorf("%s is not a valid IPv6 address", s) + } + return nil + } +} + +func IPValidator() func(string) error { + return func(str string) error { + if isIP(str) == 0 { + return fmt.Errorf("%s is not a valid IP address", str) + } + return nil + } +} + +func FQDNValidator() func(string) error { + return func(str string) error { + if !regexFQDN.MatchString(str) { + return fmt.Errorf("%s is not a valid FQDN", str) + } + return nil + } +} + +func isIP(input string) int32 { + ip := net.ParseIP(input) + if ip == nil { + return 0 + } + + if ip.To4() != nil { + return 4 + } + + return 6 +} + +func isIPv4(input string) bool { + return isIP(input) == 4 +} + +func isIPv6(input string) bool { + return isIP(input) == 6 +} diff --git a/internal/parameter/validators/str_test.go b/internal/parameter/validators/str_test.go new file mode 100644 index 0000000..069ffd0 --- /dev/null +++ b/internal/parameter/validators/str_test.go @@ -0,0 +1,149 @@ +package validators + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMaxLength(t *testing.T) { + +} + +func TestMinLength(t *testing.T) { + +} + +func TestIPv4Validator(t *testing.T) { + tests := []struct { + ip string + expected bool + }{ + {"fe80:0000:0000:0000:0204:61ff:fe9d:f156", false}, // full form of IPv6 + {"fe80:0:0:0:204:61ff:fe9d:f156", false}, // drop leading zeroes + {"fe80::204:61ff:fe9d:f156", false}, // collapse multiple zeroes to :: in the IPv6 address + {"fe80:0000:0000:0000:0204:61ff:254.157.241.86", false}, // IPv4 dotted quad at the end + {"fe80:0:0:0:0204:61ff:254.157.241.86", false}, // drop leading zeroes, IPv4 dotted quad at the end + {"fe80::204:61ff:254.157.241.86", false}, // dotted quad at the end, multiple zeroes collapsed + {"::1", false}, // localhost + {"fe80::", false}, // link-local prefix + {"2001::", false}, // global unicast prefix + {"1127.01.0.1", false}, + {"255.0:3.255", false}, + {"275.0.3.255", false}, + {"127.010.0.1", false}, + {"027.01.0.1", false}, + {"0.0.0.0", true}, + {"255.255.255.255", true}, + {"255.0.3.255", true}, + } + + for _, v := range tests { + err := IPv4Validator()(v.ip) + if v.expected { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + } +} + +func TestIPv6Validator(t *testing.T) { + tests := []struct { + ip string + expected bool + }{ + {"fe80:0000:0000:0000:0204:61ff:fe9d:f156", true}, // full form of IPv6 + {"fe80:0:0:0:204:61ff:fe9d:f156", true}, // drop leading zeroes + {"fe80::204:61ff:fe9d:f156", true}, // collapse multiple zeroes to :: in the IPv6 address + {"fe80:0000:0000:0000:0204:61ff:254.157.241.86", true}, // IPv4 dotted quad at the end + {"fe80:0:0:0:0204:61ff:254.157.241.86", true}, // drop leading zeroes, IPv4 dotted quad at the end + {"fe80::204:61ff:254.157.241.86", true}, // dotted quad at the end, multiple zeroes collapsed + {"::1", true}, // localhost + {"fe80::", true}, // link-local prefix + {"2001::", true}, // global unicast prefix + {"0.0.0.0", false}, + {"255.255.255.255", false}, + {"255.0.3.255", false}, + {"127.010.0.1", false}, + {"027.01.0.1", false}, + {"1127.01.0.1", false}, + {"255.0:3.255", false}, + {"275.0.3.255", false}, + } + + for _, v := range tests { + err := IPv6Validator()(v.ip) + if v.expected { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + } +} + +func TestIPValidator(t *testing.T) { + tests := []struct { + ip string + expected bool + }{ + {"fe80:0000:0000:0000:0204:61ff:fe9d:f156", true}, // full form of IPv6 + {"fe80:0:0:0:204:61ff:fe9d:f156", true}, // drop leading zeroes + {"fe80::204:61ff:fe9d:f156", true}, // collapse multiple zeroes to :: in the IPv6 address + {"fe80:0000:0000:0000:0204:61ff:254.157.241.86", true}, // IPv4 dotted quad at the end + {"fe80:0:0:0:0204:61ff:254.157.241.86", true}, // drop leading zeroes, IPv4 dotted quad at the end + {"fe80::204:61ff:254.157.241.86", true}, // dotted quad at the end, multiple zeroes collapsed + {"::1", true}, // localhost + {"fe80::", true}, // link-local prefix + {"2001::", true}, // global unicast prefix + {"0.0.0.0", true}, + {"255.255.255.255", true}, + {"255.0.3.255", true}, + } + + for _, v := range tests { + err := IPValidator()(v.ip) + if v.expected { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + } +} + +func TestFQDNValidator(t *testing.T) { + tests := []struct { + fqdn string + expected bool + }{ + {"test.example.com", true}, + {"example.com", true}, + {"example24.com", true}, + {"test.example24.com", true}, + {"test24.example24.com", true}, + {"test.example.com.", true}, + {"example.com.", true}, + {"example24.com.", true}, + {"test.example24.com.", true}, + {"test24.example24.com.", true}, + {"24.example24.com", true}, + {"test.24.example.com", true}, + {"test24.example24.com..", false}, + {"example", false}, + {"192.168.0.1", false}, + {"email@example.com", false}, + {"2001:cdba:0000:0000:0000:0000:3257:9652", false}, + {"2001:cdba:0:0:0:0:3257:9652", false}, + {"2001:cdba::3257:9652", false}, + {"", false}, + } + + for _, v := range tests { + err := FQDNValidator()(v.fqdn) + if v.expected { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + } +} diff --git a/internal/parameter/validators/validators.go b/internal/parameter/validators/validators.go new file mode 100644 index 0000000..35536aa --- /dev/null +++ b/internal/parameter/validators/validators.go @@ -0,0 +1,14 @@ +package validators + +type Validator[T string | []string] func(T) error + +func Group[T string | []string](validators ...Validator[T]) Validator[T] { + return func(t T) error { + for _, validator := range validators { + if err := validator(t); err != nil { + return err + } + } + return nil + } +} diff --git a/internal/render/errors.go b/internal/render/errors.go deleted file mode 100644 index a048fa1..0000000 --- a/internal/render/errors.go +++ /dev/null @@ -1,22 +0,0 @@ -package render - -import ( - "errors" - "fmt" -) - -var ( - ErrCanceled = errors.New("canceled") -) - -type MissingErr struct { - field string -} - -func (e MissingErr) Error() string { - return fmt.Sprintf("%s is required", e.field) -} - -func NewMissingErr(field string) error { - return MissingErr{field: field} -} diff --git a/internal/render/render.go b/internal/render/render.go deleted file mode 100644 index 336038e..0000000 --- a/internal/render/render.go +++ /dev/null @@ -1,18 +0,0 @@ -package render - -import ( - "strings" -) - -const ( - TypeSelect = "select" - TypeInput = "input" - TypeTextArea = "textarea" - DescMaxLength = 50 -) - -// ----------------------------------------- - -func isEmptyStr(val string) bool { - return len(strings.TrimSpace(val)) == 0 -} diff --git a/internal/render/template.go b/internal/render/template.go deleted file mode 100644 index 605d097..0000000 --- a/internal/render/template.go +++ /dev/null @@ -1,186 +0,0 @@ -package render - -import ( - "bytes" - "fmt" - "strings" - "text/template" - - "github.com/shipengqi/commitizen/internal/ui" -) - -const ( - TextAreaMaxHeight = 20 - TextWidth = 50 -) - -type Option struct { - Name string - Desc string -} - -func (o *Option) String() string { - var b strings.Builder - ml := len(o.Name) - pl := 12 - ml - 2 - padding := strings.Repeat(" ", pl) - b.WriteString(o.Name) - b.WriteString(": ") - b.WriteString(padding) - b.WriteString(o.Desc) - return b.String() -} - -type Item struct { - Name string - Desc string - Type string - Options []Option - Required bool -} - -type Template struct { - Name string - Desc string - Format string - Default bool - Items []*Item - models []model -} - -type model struct { - t string - name string - model ui.Model -} - -func NewTemplate() (*Template, error) { - t := &Template{} - err := t.init() - if err != nil { - return nil, err - } - return t, nil -} - -func (t *Template) Run(noTTY bool) ([]byte, error) { - err := t.init() - if err != nil { - return nil, err - } - - if len(t.models) == 0 { - return nil, nil - } - - values := map[string]interface{}{} - for _, v := range t.models { - if _, err = ui.Run(v.model, noTTY); err != nil { - return nil, err - } - if v.model.Canceled() { - return nil, ErrCanceled - } - val := v.model.Value() - // hardcode for the select options - if v.t == TypeSelect { - tokens := strings.Split(val, ":") - if len(tokens) > 0 { - val = tokens[0] - } - } - values[v.name] = val - } - - tmpl, err := template.New("cz").Parse(t.Format) - if err != nil { - return nil, err - } - - var buf bytes.Buffer - if err = tmpl.Execute(&buf, values); err != nil { - return nil, err - } - - return buf.Bytes(), nil -} - -func (t *Template) init() error { - if isEmptyStr(t.Format) { - return NewMissingErr("format") - } - - for _, item := range t.Items { - if isEmptyStr(item.Name) { - return NewMissingErr("item.name") - } - if isEmptyStr(item.Desc) { - return NewMissingErr("item.desc") - } - if isEmptyStr(item.Type) { - return NewMissingErr("item.type") - } - - var m ui.Model - - switch item.Type { - case TypeInput: - m = t.createInputItem(item.Name, item.Desc, item.Required) - case TypeSelect: - m = t.createSelectItem(item.Desc, item.Options) - case TypeTextArea: - m = t.createTextAreaItem(item.Name, item.Desc, item.Required) - default: - return fmt.Errorf("unsupported type: %s", item.Type) - } - t.models = append(t.models, model{ - t: item.Type, - name: item.Name, - model: m, - }) - } - return nil -} - -func (t *Template) createSelectItem(label string, options []Option) *ui.SelectModel { - var choices ui.Choices - for _, v := range options { - choices = append(choices, ui.Choice(v.String())) - } - height := 8 - if len(options) > 5 { - height = 12 - } else if len(options) > 3 { - height = 10 - } else if len(options) > 2 { - height = 9 - } - m := ui.NewSelect(label, choices).WithHeight(height) - return m -} - -func (t *Template) createInputItem(name, label string, required bool) *ui.InputModel { - m := ui.NewInput(label).WithWidth(TextWidth) - if required { - m.WithValidateFunc(NotBlankValidator(name)) - } - return m -} - -func (t *Template) createTextAreaItem(name, label string, required bool) *ui.TextAreaModel { - m := ui.NewTextArea(label).WithMaxHeight(TextAreaMaxHeight).WithWidth(TextWidth) - if required { - m.WithValidateFunc(NotBlankValidator(name)) - } - return m -} - -// NotBlankValidator is a verification function that checks whether the input is empty -func NotBlankValidator(name string) func(s string) error { - return func(s string) error { - if strings.TrimSpace(s) == "" { - return NewMissingErr(name) - } - return nil - } -} diff --git a/internal/templates/map.go b/internal/templates/map.go new file mode 100644 index 0000000..6d4e633 --- /dev/null +++ b/internal/templates/map.go @@ -0,0 +1,65 @@ +package templates + +import ( + "github.com/charmbracelet/huh" + + "github.com/shipengqi/commitizen/internal/parameter" +) + +type SortedGroupMap struct { + groups map[string]*parameter.Group + fields map[string][]huh.Field + ordered []string +} + +func NewSortedGroupMap(groups ...*parameter.Group) *SortedGroupMap { + groupmap := make(map[string]*parameter.Group) + for _, group := range groups { + groupmap[group.Name] = group + } + return &SortedGroupMap{ + groups: groupmap, + fields: make(map[string][]huh.Field), + ordered: make([]string, 0), + } +} + +func (m *SortedGroupMap) Length() int { + return len(m.ordered) +} + +func (m *SortedGroupMap) SetFields(group string, fields []huh.Field) { + if _, ok := m.fields[group]; !ok { + // save ordered group name only first time + m.ordered = append(m.ordered, group) + } + if _, ok := m.groups[group]; !ok { + // create group if not exist + m.groups[group] = parameter.NewGroup(group) + } + m.fields[group] = fields +} + +func (m *SortedGroupMap) GetFields(group string) ([]huh.Field, bool) { + exists := true + if _, ok := m.fields[group]; !ok { + exists = false + } + return m.fields[group], exists +} + +func (m *SortedGroupMap) GetGroup(group string) (*parameter.Group, bool) { + exists := true + if _, ok := m.groups[group]; !ok { + exists = false + } + return m.groups[group], exists +} + +func (m *SortedGroupMap) FormGroups(all map[parameter.FieldKey]huh.Field) []*huh.Group { + var groups []*huh.Group + for _, key := range m.ordered { + groups = append(groups, m.groups[key].Render(all, m.fields[key])) + } + return groups +} diff --git a/internal/templates/template.go b/internal/templates/template.go new file mode 100644 index 0000000..e68565e --- /dev/null +++ b/internal/templates/template.go @@ -0,0 +1,157 @@ +package templates + +import ( + "bytes" + standarderrs "errors" + "fmt" + "text/template" + + "github.com/charmbracelet/huh" + "github.com/mitchellh/mapstructure" + "github.com/shipengqi/golib/strutil" + + "github.com/shipengqi/commitizen/internal/errors" + "github.com/shipengqi/commitizen/internal/helpers" + "github.com/shipengqi/commitizen/internal/parameter" + "github.com/shipengqi/commitizen/internal/parameter/boolean" + "github.com/shipengqi/commitizen/internal/parameter/integer" + "github.com/shipengqi/commitizen/internal/parameter/list" + "github.com/shipengqi/commitizen/internal/parameter/multilist" + "github.com/shipengqi/commitizen/internal/parameter/secret" + "github.com/shipengqi/commitizen/internal/parameter/str" + "github.com/shipengqi/commitizen/internal/parameter/text" +) + +type Template struct { + all map[parameter.FieldKey]huh.Field + sorted *SortedGroupMap + + Name string + Desc string + Format string + Default bool + Items []map[string]interface{} + Groups []*parameter.Group +} + +func (t *Template) Initialize() error { + if strutil.IsEmpty(t.Format) { + return errors.NewMissingErr("format") + } + + existGroups := make(map[string]struct{}, len(t.Groups)) + existItems := make(map[string]struct{}, len(t.Items)) + + // validate the groups defined in the template + for _, v := range t.Groups { + errs := v.Validate() + if len(errs) > 0 { + return standarderrs.Join(errs...) + } + + if _, ok := existGroups[v.Name]; ok { + return fmt.Errorf("duplicate group name: %s", v.Name) + } + existGroups[v.Name] = struct{}{} + } + + // create a sorted group map + t.sorted = NewSortedGroupMap(t.Groups...) + t.all = make(map[parameter.FieldKey]huh.Field) + + for _, v := range t.Items { + namestr, err := helpers.GetValueFromYAML[string](v, "name") + if err != nil { + return err + } + + if _, ok := existItems[namestr]; ok { + return fmt.Errorf("duplicate item name: %s", namestr) + } + existItems[namestr] = struct{}{} + + typestr, err := helpers.GetValueFromYAML[string](v, "type") + if err != nil { + return err + } + var ( + param parameter.Interface + group string + ) + switch typestr { + case parameter.TypeBoolean: + param = &boolean.Param{} + err = mapstructure.Decode(v, ¶m) + case parameter.TypeString: + param = &str.Param{} + err = mapstructure.Decode(v, ¶m) + case parameter.TypeInteger: + param = &integer.Param{} + err = mapstructure.Decode(v, ¶m) + case parameter.TypeSecret: + param = &secret.Param{} + err = mapstructure.Decode(v, ¶m) + case parameter.TypeText: + param = &text.Param{} + err = mapstructure.Decode(v, ¶m) + case parameter.TypeList: + param = &list.Param{} + err = mapstructure.Decode(v, ¶m) + case parameter.TypeMultiList: + param = &multilist.Param{} + err = mapstructure.Decode(v, ¶m) + default: + return fmt.Errorf("unknown type %s", typestr) + } + if err != nil { + return err + } + errs := param.Validate() + if len(errs) > 0 { + return standarderrs.Join(errs...) + } + + group = param.GetGroup() + param.Render() + + t.all[parameter.NewFiledKey(namestr, group)] = param + + if fields, ok := t.sorted.GetFields(group); !ok { + news := make([]huh.Field, 0) + news = append(news, param) + t.sorted.SetFields(group, news) + } else { + fields = append(fields, param) + t.sorted.SetFields(group, fields) + } + } + + return nil +} + +func (t *Template) Run() ([]byte, error) { + if t.sorted.Length() == 0 { + return nil, nil + } + + values := map[string]interface{}{} + form := huh.NewForm(t.sorted.FormGroups(t.all)...) + err := form.Run() + if err != nil { + return nil, err + } + for _, field := range t.all { + values[field.GetKey()] = field.GetValue() + } + tmpl, err := template.New("cz").Parse(t.Format) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + if err = tmpl.Execute(&buf, values); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} diff --git a/internal/ui/help.go b/internal/ui/help.go deleted file mode 100644 index 47228b3..0000000 --- a/internal/ui/help.go +++ /dev/null @@ -1,35 +0,0 @@ -package ui - -import "github.com/charmbracelet/bubbles/key" - -// keyMap defines a set of keybindings. To work for help it must satisfy -// key.Map. It could also very easily be a map[string]key.Binding. -type keyMap struct { - Save key.Binding - Quit key.Binding -} - -// ShortHelp returns keybindings to be shown in the mini help view. It's part -// of the key.Map interface. -func (k keyMap) ShortHelp() []key.Binding { - return []key.Binding{k.Save, k.Quit} -} - -// FullHelp returns keybindings for the expanded help view. It's part of the -// key.Map interface. -func (k keyMap) FullHelp() [][]key.Binding { - return [][]key.Binding{ - {k.Save, k.Quit}, // first column - } -} - -var helpKeys = keyMap{ - Save: key.NewBinding( - key.WithKeys("ctrl+w", "ctrl+q"), - key.WithHelp("ctrl+w", "save"), - ), - Quit: key.NewBinding( - key.WithKeys("ctrl+c"), - key.WithHelp("ctrl+c", "quit"), - ), -} diff --git a/internal/ui/select.go b/internal/ui/select.go deleted file mode 100644 index dd9ef18..0000000 --- a/internal/ui/select.go +++ /dev/null @@ -1,154 +0,0 @@ -package ui - -import ( - "fmt" - "io" - "strings" - - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" -) - -var ( - titleStyle = lipgloss.NewStyle() - itemStyle = lipgloss.NewStyle().PaddingLeft(4) - selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170")) - paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) - helpStyle = list.DefaultStyles().HelpStyle.PaddingBottom(1) -) - -type Choices []Choice - -func (c Choices) toBubblesItem() []list.Item { - if len(c) == 0 { - return nil - } - - var items []list.Item - - for _, v := range c { - items = append(items, v) - } - return items -} - -type Choice string - -func (i Choice) FilterValue() string { return "" } - -type itemDelegate struct{} - -func (d itemDelegate) Height() int { return 1 } -func (d itemDelegate) Spacing() int { return 0 } -func (d itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } -func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { - i, ok := listItem.(Choice) - if !ok { - return - } - - str := fmt.Sprintf("%d. %s", index+1, i) - - fn := itemStyle.Render - if index == m.Index() { - fn = func(s ...string) string { - return selectedItemStyle.Render("> " + strings.Join(s, " ")) - } - } - - _, _ = fmt.Fprint(w, fn(str)) -} - -type SelectModel struct { - label string - choice string - canceled bool - err error - - list list.Model -} - -func NewSelect(label string, choices Choices) *SelectModel { - l := list.New(choices.toBubblesItem(), itemDelegate{}, DefaultSelectWidth, DefaultSelectHeight) - l.Title = label - l.SetShowStatusBar(false) - l.SetFilteringEnabled(false) - l.Styles.Title = titleStyle - l.Styles.PaginationStyle = paginationStyle - l.Styles.HelpStyle = helpStyle - - return &SelectModel{list: l, label: label} -} - -func (m *SelectModel) WithWidth(width int) *SelectModel { - m.list.SetWidth(width) - return m -} - -func (m *SelectModel) WithHeight(height int) *SelectModel { - m.list.SetHeight(height) - return m -} - -func (m *SelectModel) Init() tea.Cmd { - return nil -} - -func (m *SelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - - switch tmsg := msg.(type) { - case tea.WindowSizeMsg: - m.list.SetWidth(tmsg.Width) - return m, nil - - case tea.KeyMsg: - switch keypress := tmsg.String(); keypress { - case "q", "ctrl+c": - m.canceled = true - return m, tea.Quit - case "enter": - i, ok := m.list.SelectedItem().(Choice) - if ok { - m.choice = string(i) - } - return m, tea.Quit - } - m.list, cmd = m.list.Update(msg) - - case error: - m.err = tmsg - return m, nil - } - - return m, cmd -} - -func (m *SelectModel) View() string { - if m.choice != "" { - // hardcode for the select value - // fix https://github.com/shipengqi/commitizen/issues/18 - val := m.Value() - tokens := strings.Split(m.Value(), ":") - if len(tokens) > 0 { - val = tokens[0] - } - - return fmt.Sprintf( - "%s %s\n%s\n", - FontColor(DefaultValidateOkPrefix, colorValidateOk), - m.label, - quitValueStyle.Render(val), - ) - } - return "\n" + m.list.View() -} - -func (m *SelectModel) Value() string { - return m.choice -} - -func (m *SelectModel) Canceled() bool { - return m.canceled -} diff --git a/internal/ui/textarea.go b/internal/ui/textarea.go deleted file mode 100644 index 9ec198a..0000000 --- a/internal/ui/textarea.go +++ /dev/null @@ -1,192 +0,0 @@ -package ui - -import ( - "fmt" - - "github.com/charmbracelet/bubbles/help" - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/textarea" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" -) - -var quitValueStyle = lipgloss.NewStyle().Margin(0, 0, 0, 2) - -type TextAreaModel struct { - label string - canceled bool - finished bool - showErr bool - init bool - err error - - // validateFunc is a "real-time verification" function, which verifies - // whether the terminal input data is legal in real time - validateFunc func(string) error - - // validateOkPrefix is the prompt prefix when the validation fails - validateOkPrefix string - - // validateErrPrefix is the prompt prefix when the verification is successful - validateErrPrefix string - - helpKeys keyMap - help help.Model - input textarea.Model -} - -func NewTextArea(label string) *TextAreaModel { - ti := textarea.New() - ti.MaxHeight = DefaultTextAreaMaxHeight - ti.SetHeight(DefaultTextAreaHeight) - ti.Focus() - - return &TextAreaModel{ - input: ti, - label: label, - helpKeys: helpKeys, - help: help.New(), - validateFunc: DefaultValidateFunc, - validateOkPrefix: DefaultValidateOkPrefix, - validateErrPrefix: DefaultValidateErrPrefix, - } -} - -func (m *TextAreaModel) WithPlaceholder(placeholder string) *TextAreaModel { - m.input.Placeholder = placeholder - return m -} - -func (m *TextAreaModel) WithWidth(width int) *TextAreaModel { - m.input.SetWidth(width) - return m -} - -func (m *TextAreaModel) WithHeight(height int) *TextAreaModel { - if height > m.input.MaxHeight { - height = m.input.MaxHeight - } - m.input.SetHeight(height) - return m -} - -func (m *TextAreaModel) WithMaxHeight(height int) *TextAreaModel { - m.input.MaxHeight = height - return m -} - -func (m *TextAreaModel) WithValidateFunc(fn func(string) error) *TextAreaModel { - m.validateFunc = fn - return m -} - -func (m *TextAreaModel) WithValidateOkPrefix(prefix string) *TextAreaModel { - m.validateOkPrefix = prefix - return m -} - -func (m *TextAreaModel) WithValidateErrPrefix(prefix string) *TextAreaModel { - m.validateErrPrefix = prefix - return m -} - -func (m *TextAreaModel) Init() tea.Cmd { - return textarea.Blink -} - -func (m *TextAreaModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - // var cmds []tea.Cmd - var cmd tea.Cmd - - switch tmsg := msg.(type) { - case tea.WindowSizeMsg: - // If we set a width on the help menu it can gracefully truncate - // its view as needed. - m.help.Width = tmsg.Width - case tea.KeyMsg: - switch { - case key.Matches(tmsg, m.helpKeys.Save): - // If the real-time verification function does not return an error, - // then the input has been completed - if m.err == nil { - m.finished = true - return m, tea.Quit - } - // If there is a verification error, the error message should be display - m.showErr = true - case key.Matches(tmsg, m.helpKeys.Quit): - m.canceled = true - return m, tea.Quit - case tmsg.Type == tea.KeyEsc: - if m.input.Focused() { - m.input.Blur() - } - case tmsg.Type == tea.KeyRunes: - // Hide verification failure message when entering content again - m.showErr = false - m.err = nil - } - m.input, cmd = m.input.Update(msg) - m.err = m.validateFunc(m.input.Value()) - - // We handle errors just like any other message - // Note: msg is error only when there is an unexpected error in the underlying textinput - case error: - m.err = tmsg - m.showErr = true - return m, nil - } - - return m, cmd -} - -func (m *TextAreaModel) View() string { - if m.finished { - return fmt.Sprintf( - "%s %s\n%s\n", - FontColor(m.validateOkPrefix, colorValidateOk), - m.label, - quitValueStyle.Render(m.Value()), - ) - } - - if !m.init { - m.err = m.validateFunc(m.input.Value()) - m.init = true - } - - var showMsg, errMsg string - helpView := m.help.View(m.helpKeys) - if m.err != nil { - showMsg = fmt.Sprintf( - "%s %s\n%s", - FontColor(m.validateErrPrefix, colorValidateErr), - m.label, - m.input.View(), - ) - if m.showErr { - errMsg = FontColor(fmt.Sprintf("%s ERROR: %s\n", m.validateErrPrefix, m.err.Error()), colorValidateErr) - return fmt.Sprintf("%s\n%s\n", showMsg, errMsg) - } - } else { - showMsg = fmt.Sprintf( - "%s %s\n%s\n%s", - FontColor(m.validateOkPrefix, colorValidateOk), - m.label, - m.input.View(), - helpStyle.Render(helpView), - ) - } - - return showMsg + "\n" -} - -// Value return the input string -func (m *TextAreaModel) Value() string { - return m.input.Value() -} - -// Canceled determine whether the operation is cancelled -func (m *TextAreaModel) Canceled() bool { - return m.canceled -} diff --git a/internal/ui/textinput.go b/internal/ui/textinput.go deleted file mode 100644 index 5d0c697..0000000 --- a/internal/ui/textinput.go +++ /dev/null @@ -1,192 +0,0 @@ -package ui - -import ( - "fmt" - - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" -) - -const ( - DefaultInputWidth = 20 -) - -type InputModel struct { - label string - canceled bool - finished bool - showErr bool - init bool - err error - - // validateFunc is a "real-time verification" function, which verifies - // whether the terminal input data is legal in real time - validateFunc func(string) error - - // validateOkPrefix is the prompt prefix when the validation fails - validateOkPrefix string - - // validateErrPrefix is the prompt prefix when the verification is successful - validateErrPrefix string - - input textinput.Model -} - -func NewInput(label string) *InputModel { - ti := textinput.New() - ti.Width = DefaultInputWidth - ti.EchoMode = textinput.EchoMode(EchoNormal) - ti.Focus() - - return &InputModel{ - input: ti, - label: label, - validateFunc: DefaultValidateFunc, - validateOkPrefix: DefaultValidateOkPrefix, - validateErrPrefix: DefaultValidateErrPrefix, - } -} - -func (m *InputModel) WithPlaceholder(placeholder string) *InputModel { - m.input.Placeholder = placeholder - return m -} - -func (m *InputModel) WithValidateFunc(fn func(string) error) *InputModel { - m.validateFunc = fn - return m -} - -func (m *InputModel) WithValidateOkPrefix(prefix string) *InputModel { - m.validateOkPrefix = prefix - return m -} - -func (m *InputModel) WithValidateErrPrefix(prefix string) *InputModel { - m.validateErrPrefix = prefix - return m -} - -func (m *InputModel) WithEchoMode(mode EchoMode) *InputModel { - m.input.EchoMode = textinput.EchoMode(mode) - return m -} - -func (m *InputModel) WithWidth(width int) *InputModel { - m.input.Width = width - return m -} - -func (m *InputModel) Init() tea.Cmd { - return textinput.Blink -} - -func (m *InputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - - switch tmsg := msg.(type) { - case tea.KeyMsg: - switch tmsg.Type { - case tea.KeyCtrlC: - m.canceled = true - return m, tea.Quit - case tea.KeyEnter: - // If the real-time verification function does not return an error, - // then the input has been completed - if m.err == nil { - m.finished = true - return m, tea.Quit - } - - // If there is a verification error, the error message should be display - m.showErr = true - case tea.KeyRunes: - // Hide verification failure message when entering content again - m.showErr = false - m.err = nil - } - // Call the underlying textinput to update the terminal display - m.input, cmd = m.input.Update(msg) - // Perform real-time verification function after each input - m.err = m.validateFunc(m.input.Value()) - - // We handle errors just like any other message - // Note: msg is error only when there is an unexpected error in the underlying textinput - case error: - m.err = tmsg - m.showErr = true - return m, nil - } - - return m, cmd -} - -func (m *InputModel) View() string { - if m.finished { - switch m.EchoMode() { - case textinput.EchoNormal: - return fmt.Sprintf( - "%s %s\n%s\n", - FontColor(m.validateOkPrefix, colorValidateOk), - m.label, - quitValueStyle.Render(m.Value()), - ) - case textinput.EchoNone: - return fmt.Sprintf( - "%s %s\n", - FontColor(m.validateOkPrefix, colorValidateOk), - m.label, - ) - case textinput.EchoPassword: - return fmt.Sprintf( - "%s %s\n%s\n", - FontColor(m.validateOkPrefix, colorValidateOk), - m.label, - quitValueStyle.Render(GenMask(len([]rune(m.Value())))), - ) - } - } - - if !m.init { - m.err = m.validateFunc(m.input.Value()) - m.init = true - } - - var showMsg, errMsg string - if m.err != nil { - showMsg = fmt.Sprintf( - "%s %s\n%s", - FontColor(m.validateErrPrefix, colorValidateErr), - m.label, - m.input.View(), - ) - if m.showErr { - errMsg = FontColor(fmt.Sprintf("%s ERROR: %s\n", m.validateErrPrefix, m.err.Error()), colorValidateErr) - return fmt.Sprintf("%s\n%s\n", showMsg, errMsg) - } - } else { - showMsg = fmt.Sprintf( - "%s %s\n%s", - FontColor(m.validateOkPrefix, colorValidateOk), - m.label, - m.input.View(), - ) - } - - return showMsg + "\n" -} - -// Value return the input string -func (m *InputModel) Value() string { - return m.input.Value() -} - -// EchoMode return the input EchoMode -func (m *InputModel) EchoMode() textinput.EchoMode { - return m.input.EchoMode -} - -// Canceled determine whether the operation is cancelled -func (m *InputModel) Canceled() bool { - return m.canceled -} diff --git a/internal/ui/types.go b/internal/ui/types.go deleted file mode 100644 index 409f75f..0000000 --- a/internal/ui/types.go +++ /dev/null @@ -1,47 +0,0 @@ -package ui - -import ( - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" -) - -// EchoMode sets the input behavior of the text input field. -// EchoMode is an alias for the textinput.EchoMode. -type EchoMode textinput.EchoMode - -// Model is an alias for the tea.Model. -type Model interface { - tea.Model - - Value() string - Canceled() bool -} - -const ( - // EchoNormal displays text as is. This is the default behavior. - EchoNormal EchoMode = iota - - // EchoPassword displays the EchoCharacter mask instead of actual - // characters. This is commonly used for password fields. - EchoPassword - - // EchoNone displays nothing as characters are entered. This is commonly - // seen for password fields on the command line. - EchoNone -) - -const ( - DefaultValidateOkPrefix = "✔" - DefaultValidateErrPrefix = "✘" - DefaultTextAreaMaxHeight = 5 - DefaultTextAreaHeight = 2 - DefaultSelectWidth = 20 - DefaultSelectHeight = 12 - - ColorPrompt = "2" - colorValidateOk = "2" - colorValidateErr = "1" -) - -// DefaultValidateFunc is a verification function that does nothing -func DefaultValidateFunc(_ string) error { return nil } diff --git a/internal/ui/ui.go b/internal/ui/ui.go deleted file mode 100644 index a39bdca..0000000 --- a/internal/ui/ui.go +++ /dev/null @@ -1,13 +0,0 @@ -package ui - -import tea "github.com/charmbracelet/bubbletea" - -func Run(model tea.Model, noTTY bool) (tea.Model, error) { - var p *tea.Program - if noTTY { - p = tea.NewProgram(model, tea.WithInput(nil)) - } else { - p = tea.NewProgram(model) - } - return p.Run() -} diff --git a/internal/ui/utils.go b/internal/ui/utils.go deleted file mode 100644 index 09e44e9..0000000 --- a/internal/ui/utils.go +++ /dev/null @@ -1,24 +0,0 @@ -package ui - -import "github.com/charmbracelet/lipgloss" - -func FontColor(text, color string) string { - if text == "" { - return "" - } - return lipgloss.NewStyle().Foreground(lipgloss.Color(color)).Render(text) -} - -// GenMask generate a mask string of the specified length -func GenMask(l int) string { - return GenStr(l, "*") -} - -// GenStr generate a string of the specified length, the string is composed of the given characters -func GenStr(l int, s string) string { - var ss string - for i := 0; i < l; i++ { - ss += s - } - return ss -} diff --git a/main.go b/main.go index c76481d..9356dba 100644 --- a/main.go +++ b/main.go @@ -5,8 +5,9 @@ import ( "fmt" "os" + "github.com/charmbracelet/huh" + "github.com/shipengqi/commitizen/cmd/cz" - "github.com/shipengqi/commitizen/internal/render" ) const ( @@ -17,7 +18,7 @@ const ( func main() { err := cz.New().Execute() if err != nil { - if errors.Is(err, render.ErrCanceled) { + if errors.Is(err, huh.ErrUserAborted) { fmt.Println(err.Error()) os.Exit(ExitCodeOk) return diff --git a/test/e2e/cli_options.go b/test/e2e/cli_options.go index 46117a9..502172d 100644 --- a/test/e2e/cli_options.go +++ b/test/e2e/cli_options.go @@ -3,6 +3,5 @@ package e2e var CliOpts CliOptions type CliOptions struct { - Cli string - NoTTY int + Cli string } diff --git a/test/e2e/cz_test.go b/test/e2e/cz_test.go index 33f7b02..6c57a6f 100644 --- a/test/e2e/cz_test.go +++ b/test/e2e/cz_test.go @@ -2,19 +2,12 @@ package e2e_test import ( . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/onsi/gomega/gbytes" ) func CZTest() { - Context("Check Commitizen", func() { - It("should not need to select a template", func() { - se, err = RunCLITest() - NoError(err) - Eventually(se.Out).Should(gbytes.Say("Select the type of")) - Eventually(se.Out).Should(gbytes.Say("A new feature")) - Eventually(se.Out).ShouldNot(gbytes.Say("Select a template to use for this commit:")) - se.Terminate() - }) - }) + // Todo tea.ProgramOption will be added in the future release + // For more information: + // 1. https://github.com/charmbracelet/bubbletea/issues/761 + // 2. https://github.com/charmbracelet/huh/blob/acfe24c3f5b5cc857596778d7885051f9a0d19c1/form.go#L283C1-L287C2 + Context("Run Commitizen", func() {}) } diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index 3215eb5..c47ee73 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -20,7 +20,6 @@ var ( func init() { flag.StringVar(&CliOpts.Cli, "cli", "", "path to the commitizen command to use.") - flag.IntVar(&CliOpts.NoTTY, "no-tty", 0, "make sure that the TTY (terminal) is never used for any output.") } var _ = Describe("Sorted Tests", func() { @@ -57,9 +56,6 @@ func RunCLITestAndWait(args ...string) (*gexec.Session, error) { } func RunCLITest(args ...string) (*gexec.Session, error) { - if CliOpts.NoTTY == 1 { - args = append(args, "--no-tty") - } cmd := exec.Command(CliOpts.Cli, args...) return gexec.Start(cmd, GinkgoWriter, GinkgoWriter) } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy