feat: update tui
This commit is contained in:
@@ -73,14 +73,6 @@ func parseReleaseCLIArgs(args []string) ([]string, release.ExecutionOptions, boo
|
|||||||
switch arg {
|
switch arg {
|
||||||
case "select":
|
case "select":
|
||||||
selectMode = true
|
selectMode = true
|
||||||
case "--dry-run":
|
|
||||||
execution.DryRun = true
|
|
||||||
case "--commit":
|
|
||||||
execution.Commit = true
|
|
||||||
case "--tag":
|
|
||||||
execution.Tag = true
|
|
||||||
case "--push":
|
|
||||||
execution.Push = true
|
|
||||||
default:
|
default:
|
||||||
if strings.HasPrefix(arg, "--") {
|
if strings.HasPrefix(arg, "--") {
|
||||||
return nil, release.ExecutionOptions{}, false, fmt.Errorf("unknown flag %q", arg)
|
return nil, release.ExecutionOptions{}, false, fmt.Errorf("unknown flag %q", arg)
|
||||||
|
|||||||
@@ -256,13 +256,16 @@ func TestRunnerExecutesReleaseFlow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunnerLeavesChangesUncommittedByDefault(t *testing.T) {
|
func TestRunnerAlwaysCommitsTagsAndPushes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
|
remote := filepath.Join(t.TempDir(), "remote.git")
|
||||||
mustRun(t, root, "git", "init")
|
mustRun(t, root, "git", "init")
|
||||||
mustRun(t, root, "git", "config", "user.name", "Release Test")
|
mustRun(t, root, "git", "config", "user.name", "Release Test")
|
||||||
mustRun(t, root, "git", "config", "user.email", "release-test@example.com")
|
mustRun(t, root, "git", "config", "user.email", "release-test@example.com")
|
||||||
|
mustRun(t, root, "git", "config", "commit.gpgsign", "false")
|
||||||
|
mustRun(t, root, "git", "config", "tag.gpgsign", "false")
|
||||||
if err := os.WriteFile(filepath.Join(root, "flake.nix"), []byte("{ outputs = { self }: {}; }\n"), 0o644); err != nil {
|
if err := os.WriteFile(filepath.Join(root, "flake.nix"), []byte("{ outputs = { self }: {}; }\n"), 0o644); err != nil {
|
||||||
t.Fatalf("WriteFile(flake.nix): %v", err)
|
t.Fatalf("WriteFile(flake.nix): %v", err)
|
||||||
}
|
}
|
||||||
@@ -271,6 +274,9 @@ func TestRunnerLeavesChangesUncommittedByDefault(t *testing.T) {
|
|||||||
}
|
}
|
||||||
mustRun(t, root, "git", "add", "-A")
|
mustRun(t, root, "git", "add", "-A")
|
||||||
mustRun(t, root, "git", "commit", "-m", "init")
|
mustRun(t, root, "git", "commit", "-m", "init")
|
||||||
|
mustRun(t, root, "git", "init", "--bare", remote)
|
||||||
|
mustRun(t, root, "git", "remote", "add", "origin", remote)
|
||||||
|
mustRun(t, root, "git", "push", "-u", "origin", "HEAD")
|
||||||
|
|
||||||
binDir := t.TempDir()
|
binDir := t.TempDir()
|
||||||
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
||||||
@@ -296,58 +302,20 @@ func TestRunnerLeavesChangesUncommittedByDefault(t *testing.T) {
|
|||||||
|
|
||||||
assertFileEquals(t, filepath.Join(root, "VERSION"), "1.0.1\nstable\n0\n")
|
assertFileEquals(t, filepath.Join(root, "VERSION"), "1.0.1\nstable\n0\n")
|
||||||
status := strings.TrimSpace(mustOutput(t, root, "git", "status", "--short"))
|
status := strings.TrimSpace(mustOutput(t, root, "git", "status", "--short"))
|
||||||
if status != "M VERSION" {
|
|
||||||
t.Fatalf("git status --short = %q, want %q", status, "M VERSION")
|
|
||||||
}
|
|
||||||
|
|
||||||
tagList := strings.TrimSpace(mustOutput(t, root, "git", "tag", "--list"))
|
|
||||||
if tagList != "" {
|
|
||||||
t.Fatalf("git tag --list = %q, want empty", tagList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunnerDryRunDoesNotModifyRepo(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
root := t.TempDir()
|
|
||||||
mustRun(t, root, "git", "init")
|
|
||||||
mustRun(t, root, "git", "config", "user.name", "Release Test")
|
|
||||||
mustRun(t, root, "git", "config", "user.email", "release-test@example.com")
|
|
||||||
if err := os.WriteFile(filepath.Join(root, "flake.nix"), []byte("{ outputs = { self }: {}; }\n"), 0o644); err != nil {
|
|
||||||
t.Fatalf("WriteFile(flake.nix): %v", err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(filepath.Join(root, "VERSION"), []byte("1.0.0\nstable\n0\n"), 0o644); err != nil {
|
|
||||||
t.Fatalf("WriteFile(VERSION): %v", err)
|
|
||||||
}
|
|
||||||
mustRun(t, root, "git", "add", "-A")
|
|
||||||
mustRun(t, root, "git", "commit", "-m", "init")
|
|
||||||
|
|
||||||
var stdout strings.Builder
|
|
||||||
r := &Runner{
|
|
||||||
Config: Config{
|
|
||||||
RootDir: root,
|
|
||||||
AllowedChannels: []string{"alpha", "beta", "rc", "internal"},
|
|
||||||
Execution: ExecutionOptions{
|
|
||||||
DryRun: true,
|
|
||||||
Commit: true,
|
|
||||||
Tag: true,
|
|
||||||
Push: true,
|
|
||||||
},
|
|
||||||
Stdout: &stdout,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.Run([]string{"patch"}); err != nil {
|
|
||||||
t.Fatalf("Runner.Run: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertFileEquals(t, filepath.Join(root, "VERSION"), "1.0.0\nstable\n0\n")
|
|
||||||
status := strings.TrimSpace(mustOutput(t, root, "git", "status", "--short"))
|
|
||||||
if status != "" {
|
if status != "" {
|
||||||
t.Fatalf("git status --short = %q, want empty", status)
|
t.Fatalf("git status --short = %q, want empty", status)
|
||||||
}
|
}
|
||||||
if !strings.Contains(stdout.String(), "Dry run: 1.0.1") {
|
|
||||||
t.Fatalf("dry-run output missing next version:\n%s", stdout.String())
|
tagList := strings.TrimSpace(mustOutput(t, root, "git", "tag", "--list", "v1.0.1"))
|
||||||
|
if tagList != "v1.0.1" {
|
||||||
|
t.Fatalf("git tag --list v1.0.1 = %q, want v1.0.1", tagList)
|
||||||
|
}
|
||||||
|
|
||||||
|
branch := strings.TrimSpace(mustOutput(t, root, "git", "branch", "--show-current"))
|
||||||
|
remoteHead := strings.TrimSpace(mustOutput(t, root, "git", "rev-parse", "origin/"+branch))
|
||||||
|
localHead := strings.TrimSpace(mustOutput(t, root, "git", "rev-parse", "HEAD"))
|
||||||
|
if remoteHead != localHead {
|
||||||
|
t.Fatalf("origin/%s = %q, want %q", branch, remoteHead, localHead)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExecutionOptions struct {
|
type ExecutionOptions struct {
|
||||||
DryRun bool
|
|
||||||
Commit bool
|
Commit bool
|
||||||
Tag bool
|
Tag bool
|
||||||
Push bool
|
Push bool
|
||||||
@@ -31,13 +30,11 @@ type Runner struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o ExecutionOptions) Normalize() ExecutionOptions {
|
func (o ExecutionOptions) Normalize() ExecutionOptions {
|
||||||
if o.Push {
|
return ExecutionOptions{
|
||||||
o.Commit = true
|
Commit: true,
|
||||||
|
Tag: true,
|
||||||
|
Push: true,
|
||||||
}
|
}
|
||||||
if o.Tag {
|
|
||||||
o.Commit = true
|
|
||||||
}
|
|
||||||
return o
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Runner) Run(args []string) error {
|
func (r *Runner) Run(args []string) error {
|
||||||
@@ -60,11 +57,6 @@ func (r *Runner) Run(args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if execution.DryRun {
|
|
||||||
printReleasePlan(stdout, nextVersion, execution, strings.TrimSpace(r.Config.ReleaseStepsJSON) != "", strings.TrimSpace(r.Config.PostVersion) != "")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := requireCleanGit(rootDir); err != nil {
|
if err := requireCleanGit(rootDir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -114,10 +106,6 @@ func (r *Runner) finalizeRelease(rootDir string, version Version, execution Exec
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !execution.Commit {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "add", "-A"); err != nil {
|
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "add", "-A"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -127,34 +115,15 @@ func (r *Runner) finalizeRelease(rootDir string, version Version, execution Exec
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if execution.Tag {
|
|
||||||
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "tag", version.Tag()); err != nil {
|
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "tag", version.Tag()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if !execution.Push {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "push"); err != nil {
|
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "push"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if execution.Tag {
|
|
||||||
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "push", "--tags"); err != nil {
|
if _, err := runCommand(rootDir, r.Config.Env, stdout, stderr, "git", "push", "--tags"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func printReleasePlan(stdout io.Writer, version Version, execution ExecutionOptions, hasReleaseSteps bool, hasPostVersion bool) {
|
|
||||||
fmt.Fprintf(stdout, "Dry run: %s\n", version.String())
|
|
||||||
fmt.Fprintf(stdout, "Tag: %s\n", version.Tag())
|
|
||||||
fmt.Fprintf(stdout, "Release steps: %s\n", yesNo(hasReleaseSteps))
|
|
||||||
fmt.Fprintf(stdout, "Post-version: %s\n", yesNo(hasPostVersion))
|
|
||||||
fmt.Fprintf(stdout, "nix fmt: yes\n")
|
|
||||||
fmt.Fprintf(stdout, "git commit: %s\n", yesNo(execution.Commit))
|
|
||||||
fmt.Fprintf(stdout, "git tag: %s\n", yesNo(execution.Tag))
|
|
||||||
fmt.Fprintf(stdout, "git push: %s\n", yesNo(execution.Push))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func SelectCommand(config Config) ([]string, bool, error) {
|
|||||||
return nil, false, fmt.Errorf("no release commands available for current version %s", versionFile.Version.String())
|
return nil, false, fmt.Errorf("no release commands available for current version %s", versionFile.Version.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
model := newCommandPickerModel(versionFile.Version, options)
|
model := newCommandPickerModel(config, versionFile.Version)
|
||||||
finalModel, err := tea.NewProgram(model, tea.WithAltScreen()).Run()
|
finalModel, err := tea.NewProgram(model, tea.WithAltScreen()).Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
@@ -111,18 +111,6 @@ func formatReleaseCommand(args []string) string {
|
|||||||
func formatReleaseCommandWithExecution(args []string, execution ExecutionOptions) string {
|
func formatReleaseCommandWithExecution(args []string, execution ExecutionOptions) string {
|
||||||
var parts []string
|
var parts []string
|
||||||
parts = append(parts, "release")
|
parts = append(parts, "release")
|
||||||
if execution.DryRun {
|
|
||||||
parts = append(parts, "--dry-run")
|
|
||||||
}
|
|
||||||
if execution.Commit {
|
|
||||||
parts = append(parts, "--commit")
|
|
||||||
}
|
|
||||||
if execution.Tag {
|
|
||||||
parts = append(parts, "--tag")
|
|
||||||
}
|
|
||||||
if execution.Push {
|
|
||||||
parts = append(parts, "--push")
|
|
||||||
}
|
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return strings.Join(parts, " ")
|
return strings.Join(parts, " ")
|
||||||
}
|
}
|
||||||
@@ -199,7 +187,6 @@ func buildPreview(config Config, current Version, args []string, next Version) s
|
|||||||
" git commit: "+yesNo(execution.Commit),
|
" git commit: "+yesNo(execution.Commit),
|
||||||
" git tag: "+yesNo(execution.Tag),
|
" git tag: "+yesNo(execution.Tag),
|
||||||
" git push: "+yesNo(execution.Push),
|
" git push: "+yesNo(execution.Push),
|
||||||
" dry run: "+yesNo(execution.DryRun),
|
|
||||||
)
|
)
|
||||||
return strings.Join(lines, "\n")
|
return strings.Join(lines, "\n")
|
||||||
}
|
}
|
||||||
@@ -224,19 +211,32 @@ func capitalize(s string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type commandPickerModel struct {
|
type commandPickerModel struct {
|
||||||
|
config Config
|
||||||
current Version
|
current Version
|
||||||
options []CommandOption
|
|
||||||
cursor int
|
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
|
focusSection int
|
||||||
|
focusIndex int
|
||||||
|
bumpOptions []selectionOption
|
||||||
|
channelOptions []selectionOption
|
||||||
|
bumpCursor int
|
||||||
|
channelCursor int
|
||||||
confirmed bool
|
confirmed bool
|
||||||
selected CommandOption
|
selected CommandOption
|
||||||
|
err string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCommandPickerModel(current Version, options []CommandOption) commandPickerModel {
|
type selectionOption struct {
|
||||||
|
Label string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCommandPickerModel(config Config, current Version) commandPickerModel {
|
||||||
return commandPickerModel{
|
return commandPickerModel{
|
||||||
|
config: config,
|
||||||
current: current,
|
current: current,
|
||||||
options: options,
|
bumpOptions: buildBumpOptions(current),
|
||||||
|
channelOptions: buildChannelOptions(current, config.AllowedChannels),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,17 +253,20 @@ func (m commandPickerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "ctrl+c", "q", "esc":
|
case "ctrl+c", "q", "esc":
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
case "up", "k":
|
case "up", "k", "shift+tab":
|
||||||
if m.cursor > 0 {
|
m.moveFocus(-1)
|
||||||
m.cursor--
|
case "down", "j", "tab":
|
||||||
}
|
m.moveFocus(1)
|
||||||
case "down", "j":
|
case " ":
|
||||||
if m.cursor < len(m.options)-1 {
|
m.selectFocused()
|
||||||
m.cursor++
|
|
||||||
}
|
|
||||||
case "enter":
|
case "enter":
|
||||||
|
option, err := m.selectedOption()
|
||||||
|
if err != nil {
|
||||||
|
m.err = err.Error()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
m.confirmed = true
|
m.confirmed = true
|
||||||
m.selected = m.options[m.cursor]
|
m.selected = option
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,28 +274,180 @@ func (m commandPickerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m commandPickerModel) View() string {
|
func (m commandPickerModel) View() string {
|
||||||
if len(m.options) == 0 {
|
if len(m.bumpOptions) == 0 || len(m.channelOptions) == 0 {
|
||||||
return "No release commands available.\n"
|
return "No release commands available.\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
preview := m.options[m.cursor].Preview
|
preview := m.preview()
|
||||||
header := fmt.Sprintf("Release command picker\nCurrent version: %s\nUse up/down or j/k to choose, Enter to run, q to cancel.\n", m.current.String())
|
header := fmt.Sprintf("Release command picker\nCurrent version: %s\nUse up/down to move through options, Space to select, Enter to run, q to cancel.\n", m.current.String())
|
||||||
|
sections := strings.Join([]string{
|
||||||
listLines := make([]string, 0, len(m.options)+1)
|
m.renderSection("Bump type", m.bumpOptions, m.bumpCursor, m.focusSection == 0, m.focusedOptionIndex()),
|
||||||
listLines = append(listLines, "Commands")
|
"",
|
||||||
for i, option := range m.options {
|
m.renderSection("Channel", m.channelOptions, m.channelCursor, m.focusSection == 1, m.focusedOptionIndex()),
|
||||||
cursor := " "
|
}, "\n")
|
||||||
if i == m.cursor {
|
|
||||||
cursor = "> "
|
|
||||||
}
|
|
||||||
listLines = append(listLines, fmt.Sprintf("%s%s\n %s", cursor, option.Command, option.Description))
|
|
||||||
}
|
|
||||||
list := strings.Join(listLines, "\n")
|
|
||||||
|
|
||||||
if m.width >= 100 {
|
if m.width >= 100 {
|
||||||
return header + "\n" + renderColumns(list, preview, m.width)
|
return header + "\n" + renderColumns(sections, preview, m.width)
|
||||||
}
|
}
|
||||||
return header + "\n" + list + "\n\n" + preview + "\n"
|
return header + "\n" + sections + "\n\n" + preview + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildBumpOptions(current Version) []selectionOption {
|
||||||
|
options := []selectionOption{
|
||||||
|
{Label: "Patch", Value: "patch"},
|
||||||
|
{Label: "Minor", Value: "minor"},
|
||||||
|
{Label: "Major", Value: "major"},
|
||||||
|
}
|
||||||
|
if current.Channel != "stable" {
|
||||||
|
options = append(options, selectionOption{
|
||||||
|
Label: "None",
|
||||||
|
Value: "",
|
||||||
|
})
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
options = append(options, selectionOption{
|
||||||
|
Label: "None",
|
||||||
|
Value: "",
|
||||||
|
})
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildChannelOptions(current Version, allowedChannels []string) []selectionOption {
|
||||||
|
options := []selectionOption{{
|
||||||
|
Label: "Current",
|
||||||
|
Value: current.Channel,
|
||||||
|
}}
|
||||||
|
if current.Channel != "stable" {
|
||||||
|
options = append(options, selectionOption{
|
||||||
|
Label: "Stable",
|
||||||
|
Value: "stable",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, channel := range allowedChannels {
|
||||||
|
if channel == current.Channel {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
options = append(options, selectionOption{
|
||||||
|
Label: capitalize(channel),
|
||||||
|
Value: channel,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *commandPickerModel) moveFocus(delta int) {
|
||||||
|
total := len(m.bumpOptions) + len(m.channelOptions)
|
||||||
|
if total == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
index := wrapIndex(m.focusIndex+delta, total)
|
||||||
|
m.focusIndex = index
|
||||||
|
|
||||||
|
if index < len(m.bumpOptions) {
|
||||||
|
m.focusSection = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.focusSection = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *commandPickerModel) selectFocused() {
|
||||||
|
if m.focusSection == 0 {
|
||||||
|
m.bumpCursor = m.focusedOptionIndex()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.channelCursor = m.focusedOptionIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapIndex(idx int, size int) int {
|
||||||
|
if size == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
for idx < 0 {
|
||||||
|
idx += size
|
||||||
|
}
|
||||||
|
return idx % size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m commandPickerModel) focusedOptionIndex() int {
|
||||||
|
if m.focusSection == 0 {
|
||||||
|
return m.focusIndex
|
||||||
|
}
|
||||||
|
return m.focusIndex - len(m.bumpOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m commandPickerModel) renderSection(title string, options []selectionOption, cursor int, focused bool, focusedIndex int) string {
|
||||||
|
lines := []string{title}
|
||||||
|
for i, option := range options {
|
||||||
|
pointer := " "
|
||||||
|
if focused && i == focusedIndex {
|
||||||
|
pointer = ">"
|
||||||
|
}
|
||||||
|
radio := "( )"
|
||||||
|
if i == cursor {
|
||||||
|
radio = "(*)"
|
||||||
|
}
|
||||||
|
lines = append(lines, fmt.Sprintf("%s %s %s", pointer, radio, option.Label))
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m commandPickerModel) selectedArgs() []string {
|
||||||
|
bump := m.bumpOptions[m.bumpCursor].Value
|
||||||
|
channel := m.channelOptions[m.channelCursor].Value
|
||||||
|
|
||||||
|
if bump == "" {
|
||||||
|
if channel == "stable" {
|
||||||
|
return []string{"stable"}
|
||||||
|
}
|
||||||
|
if channel == m.current.Channel {
|
||||||
|
if channel == "stable" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []string{channel}
|
||||||
|
}
|
||||||
|
return []string{channel}
|
||||||
|
}
|
||||||
|
|
||||||
|
if channel == m.current.Channel || (channel == "stable" && m.current.Channel == "stable") {
|
||||||
|
return []string{bump}
|
||||||
|
}
|
||||||
|
return []string{bump, channel}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m commandPickerModel) selectedOption() (CommandOption, error) {
|
||||||
|
args := m.selectedArgs()
|
||||||
|
next, err := ResolveNextVersion(m.current, args, m.config.AllowedChannels)
|
||||||
|
if err != nil {
|
||||||
|
return CommandOption{}, err
|
||||||
|
}
|
||||||
|
return CommandOption{
|
||||||
|
Title: titleForArgs(args),
|
||||||
|
Description: descriptionForArgs(m.current, args, next),
|
||||||
|
Command: formatReleaseCommand(args),
|
||||||
|
Args: append([]string(nil), args...),
|
||||||
|
NextVersion: next,
|
||||||
|
Preview: buildPreview(m.config, m.current, args, next),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m commandPickerModel) preview() string {
|
||||||
|
option, err := m.selectedOption()
|
||||||
|
if err != nil {
|
||||||
|
lines := []string{
|
||||||
|
"Command",
|
||||||
|
" " + formatReleaseCommand(m.selectedArgs()),
|
||||||
|
"",
|
||||||
|
"Selection",
|
||||||
|
" " + err.Error(),
|
||||||
|
}
|
||||||
|
if m.err != "" {
|
||||||
|
lines = append(lines, "", "Error", " "+m.err)
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
return option.Preview
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderColumns(left string, right string, width int) string {
|
func renderColumns(left string, right string, width int) string {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package release
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBuildCommandOptionsForStableVersion(t *testing.T) {
|
func TestBuildCommandOptionsForStableVersion(t *testing.T) {
|
||||||
@@ -81,6 +83,125 @@ func TestBuildCommandOptionsForPrereleaseVersion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCommandPickerSelectionForStableVersion(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
model := newCommandPickerModel(Config{
|
||||||
|
AllowedChannels: []string{"alpha", "beta"},
|
||||||
|
}, MustParseVersion(t, "1.2.3"))
|
||||||
|
|
||||||
|
model.bumpCursor = 1
|
||||||
|
model.channelCursor = 0
|
||||||
|
|
||||||
|
option, err := model.selectedOption()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("selectedOption(): %v", err)
|
||||||
|
}
|
||||||
|
if got := strings.Join(option.Args, " "); got != "minor" {
|
||||||
|
t.Fatalf("selected args = %q, want %q", got, "minor")
|
||||||
|
}
|
||||||
|
if option.NextVersion.String() != "1.3.0" {
|
||||||
|
t.Fatalf("next version = %q, want %q", option.NextVersion.String(), "1.3.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
model.bumpCursor = 3
|
||||||
|
model.channelCursor = 1
|
||||||
|
|
||||||
|
option, err = model.selectedOption()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("selectedOption(channel only): %v", err)
|
||||||
|
}
|
||||||
|
if got := strings.Join(option.Args, " "); got != "alpha" {
|
||||||
|
t.Fatalf("selected args = %q, want %q", got, "alpha")
|
||||||
|
}
|
||||||
|
if option.NextVersion.String() != "1.2.4-alpha.1" {
|
||||||
|
t.Fatalf("next version = %q, want %q", option.NextVersion.String(), "1.2.4-alpha.1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommandPickerSelectionForPrereleaseVersion(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
model := newCommandPickerModel(Config{
|
||||||
|
AllowedChannels: []string{"alpha", "beta", "rc"},
|
||||||
|
}, MustParseVersion(t, "1.2.3-beta.2"))
|
||||||
|
|
||||||
|
model.bumpCursor = 3
|
||||||
|
model.channelCursor = 0
|
||||||
|
|
||||||
|
option, err := model.selectedOption()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("selectedOption(current prerelease): %v", err)
|
||||||
|
}
|
||||||
|
if got := strings.Join(option.Args, " "); got != "beta" {
|
||||||
|
t.Fatalf("selected args = %q, want %q", got, "beta")
|
||||||
|
}
|
||||||
|
if option.NextVersion.String() != "1.2.3-beta.3" {
|
||||||
|
t.Fatalf("next version = %q, want %q", option.NextVersion.String(), "1.2.3-beta.3")
|
||||||
|
}
|
||||||
|
|
||||||
|
model.channelCursor = 1
|
||||||
|
|
||||||
|
option, err = model.selectedOption()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("selectedOption(promote): %v", err)
|
||||||
|
}
|
||||||
|
if got := strings.Join(option.Args, " "); got != "stable" {
|
||||||
|
t.Fatalf("selected args = %q, want %q", got, "stable")
|
||||||
|
}
|
||||||
|
if option.NextVersion.String() != "1.2.3" {
|
||||||
|
t.Fatalf("next version = %q, want %q", option.NextVersion.String(), "1.2.3")
|
||||||
|
}
|
||||||
|
|
||||||
|
model.bumpCursor = 0
|
||||||
|
|
||||||
|
if _, err := model.selectedOption(); err == nil {
|
||||||
|
t.Fatalf("selectedOption(patch stable from prerelease) succeeded, want error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommandPickerFocusMovesAcrossSections(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
model := newCommandPickerModel(Config{
|
||||||
|
AllowedChannels: []string{"alpha", "beta"},
|
||||||
|
}, MustParseVersion(t, "1.2.3"))
|
||||||
|
|
||||||
|
next, _ := model.Update(tea.KeyMsg{Type: tea.KeyDown})
|
||||||
|
model = next.(commandPickerModel)
|
||||||
|
if model.focusSection != 0 || model.focusIndex != 1 || model.bumpCursor != 0 {
|
||||||
|
t.Fatalf("after first down: focusSection=%d focusIndex=%d bumpCursor=%d", model.focusSection, model.focusIndex, model.bumpCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
next, _ = model.Update(tea.KeyMsg{Type: tea.KeyDown})
|
||||||
|
model = next.(commandPickerModel)
|
||||||
|
next, _ = model.Update(tea.KeyMsg{Type: tea.KeyDown})
|
||||||
|
model = next.(commandPickerModel)
|
||||||
|
next, _ = model.Update(tea.KeyMsg{Type: tea.KeyDown})
|
||||||
|
model = next.(commandPickerModel)
|
||||||
|
if model.focusSection != 1 || model.focusIndex != 4 || model.channelCursor != 0 {
|
||||||
|
t.Fatalf("after moving into channel section: focusSection=%d focusIndex=%d channelCursor=%d", model.focusSection, model.focusIndex, model.channelCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
next, _ = model.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||||
|
model = next.(commandPickerModel)
|
||||||
|
if model.channelCursor != 0 {
|
||||||
|
t.Fatalf("space changed selection unexpectedly: channelCursor=%d", model.channelCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
next, _ = model.Update(tea.KeyMsg{Type: tea.KeyUp})
|
||||||
|
model = next.(commandPickerModel)
|
||||||
|
if model.focusSection != 0 || model.focusIndex != 3 || model.bumpCursor != 0 {
|
||||||
|
t.Fatalf("after moving back up: focusSection=%d focusIndex=%d bumpCursor=%d", model.focusSection, model.focusIndex, model.bumpCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
next, _ = model.Update(tea.KeyMsg{Type: tea.KeySpace})
|
||||||
|
model = next.(commandPickerModel)
|
||||||
|
if model.bumpCursor != 3 {
|
||||||
|
t.Fatalf("space did not update bump selection: bumpCursor=%d", model.bumpCursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func findOptionByCommand(options []CommandOption, command string) (CommandOption, bool) {
|
func findOptionByCommand(options []CommandOption, command string) (CommandOption, bool) {
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
if option.Command == command {
|
if option.Command == command {
|
||||||
|
|||||||
Reference in New Issue
Block a user