Skip to content

Commit a6d04c7

Browse files
Add config writer command
Signed-off-by: Steven A Coffman <gears@umich.edu> Signed-off-by: Steve Coffman <steve@khanacademy.org> Signed-off-by: Steve Coffman <steve@khanacademy.org>
1 parent 6d9689a commit a6d04c7

File tree

17 files changed

+757
-305
lines changed

17 files changed

+757
-305
lines changed

README.md

+7-5
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22

33
`jt` is a CLI tool for viewing and manipulating JIRA issues.
44

5-
An example usage to transition an issue to a new status:
5+
One common example usage to transition an issue to a new status:
66
```
77
jt "In Progress" TEAM-1234
88
```
99

10-
If you are in a git repository on a topic branch who's name matches `team-1234[-whatever]`, you can omit
10+
If you are in a git repository where the topic branch's name matches `[whatever-]team-1234[-whatever]`, you can omit
1111
the issue argument as it is implied.
1212

13-
### Usage:
13+
Yeah, we even let you use underscores.
14+
15+
### Common Usage:
1416
jt [new state] [issue number]
1517

1618
**Note:**
@@ -30,11 +32,11 @@ JIRA board's workflow.
3032
### Other Available Commands:
3133
| command | what it does |
3234
|---|---|
33-
| completion | generate the autocompletion script for the specified shell |
34-
| help | Help about any command |
3535
| onit | Self-assign and transition an issue to In Progress status |
3636
| take | Assign an issue to you |
3737
| wti | What The Issue? - View an issue in Github Markdown |
38+
| completion | generate the autocompletion script for the specified shell |
39+
| help | Help about any command |
3840

3941
Shared Flags:
4042
| flag | what it does |

cmd/config.go

+252
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
8+
"github.com/StevenACoffman/jt/pkg/atlassian"
9+
"github.com/StevenACoffman/jt/pkg/colors"
10+
11+
"github.com/charmbracelet/bubbles/textinput"
12+
tea "github.com/charmbracelet/bubbletea"
13+
"github.com/charmbracelet/lipgloss"
14+
"github.com/spf13/cobra"
15+
)
16+
17+
// configCmd represents the config command
18+
var configCmd = &cobra.Command{
19+
Use: "config",
20+
Short: "Save your JIRA config for use in other commands",
21+
Long: `This will ask for your JIRA token, tenant URL and email.
22+
It will backup any existing config file and make a new one.`,
23+
Run: func(cmd *cobra.Command, args []string) {
24+
configure()
25+
os.Exit(exitSuccess)
26+
},
27+
}
28+
29+
func configure() {
30+
if atlassian.CheckConfigFileExists(cfgFile) {
31+
32+
backupErr := BackupConfigFile(cfgFile)
33+
if backupErr != nil {
34+
fmt.Println("Unable to backup config file!")
35+
os.Exit(exitFail)
36+
}
37+
}
38+
model := initialModel()
39+
40+
if err := tea.NewProgram(&model).Start(); err != nil {
41+
fmt.Printf("could not start program: %s\n", err)
42+
os.Exit(1)
43+
}
44+
45+
err := atlassian.SaveConfig(cfgFile, jiraConfig)
46+
if err != nil {
47+
fmt.Println(err)
48+
os.Exit(exitFail)
49+
}
50+
jiraClient = atlassian.GetJIRAClient(jiraConfig)
51+
fmt.Println("Successfully wrote config to ", cfgFile)
52+
}
53+
54+
func init() {
55+
rootCmd.AddCommand(configCmd)
56+
57+
// Here you will define your flags and configuration settings.
58+
59+
// Cobra supports Persistent Flags which will work for this command
60+
// and all subcommands, e.g.:
61+
// configCmd.PersistentFlags().String("foo", "", "A help for foo")
62+
63+
// Cobra supports local flags which will only run when this command
64+
// is called directly, e.g.:
65+
// configCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
66+
}
67+
68+
var (
69+
focusedStyle = lipgloss.NewStyle().Foreground(
70+
lipgloss.AdaptiveColor{
71+
Light: colors.ANSIGreen.String(),
72+
Dark: colors.ANSIBrightGreen.String(),
73+
})
74+
blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) //#585858
75+
cursorStyle = focusedStyle.Copy()
76+
noStyle = lipgloss.NewStyle()
77+
helpStyle = blurredStyle.Copy()
78+
cursorModeHelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244")) //#808080
79+
80+
focusedButton = focusedStyle.Copy().Render("[ Submit ]")
81+
blurredButton = fmt.Sprintf("[ %s ]", blurredStyle.Render("Submit"))
82+
)
83+
84+
type model struct {
85+
focusIndex int
86+
inputs []textinput.Model
87+
cursorMode textinput.CursorMode
88+
choice chan *atlassian.Config
89+
}
90+
91+
func initialModel() model {
92+
m := model{
93+
inputs: make([]textinput.Model, 3),
94+
}
95+
96+
var t textinput.Model
97+
for i := range m.inputs {
98+
t = textinput.NewModel()
99+
t.CursorStyle = cursorStyle
100+
t.CharLimit = 128
101+
102+
switch i {
103+
case 0:
104+
t.Placeholder = "Paste token here"
105+
t.Focus()
106+
t.PromptStyle = focusedStyle
107+
t.TextStyle = focusedStyle
108+
t.EchoMode = textinput.EchoPassword
109+
t.EchoCharacter = '•'
110+
case 1:
111+
t.Placeholder = "Host URL like https://tenant.atlassian.net"
112+
case 2:
113+
t.Placeholder = "Email"
114+
}
115+
116+
m.inputs[i] = t
117+
}
118+
119+
return m
120+
}
121+
122+
func (m model) Init() tea.Cmd {
123+
return textinput.Blink
124+
}
125+
126+
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
127+
switch msg := msg.(type) {
128+
case tea.KeyMsg:
129+
switch msg.String() {
130+
case "ctrl+c", "esc":
131+
return m, tea.Quit
132+
133+
// Change cursor mode
134+
case "ctrl+r":
135+
m.cursorMode++
136+
if m.cursorMode > textinput.CursorHide {
137+
m.cursorMode = textinput.CursorBlink
138+
}
139+
cmds := make([]tea.Cmd, len(m.inputs))
140+
for i := range m.inputs {
141+
cmds[i] = m.inputs[i].SetCursorMode(m.cursorMode)
142+
}
143+
return m, tea.Batch(cmds...)
144+
145+
// Set focus to next input
146+
case "tab", "shift+tab", "enter", "up", "down":
147+
s := msg.String()
148+
149+
// Did the user press enter while the submit button was focused?
150+
// If so, save choices and exit.
151+
if s == "enter" && m.focusIndex == len(m.inputs) {
152+
if jiraConfig == nil {
153+
jiraConfig = &atlassian.Config{}
154+
}
155+
for i, input := range m.inputs {
156+
switch i {
157+
case 0:
158+
jiraConfig.Token = input.Value()
159+
case 1:
160+
jiraConfig.Host = input.Value()
161+
case 2:
162+
jiraConfig.User = input.Value()
163+
}
164+
}
165+
return m, tea.Quit
166+
}
167+
168+
// Cycle indexes
169+
if s == "up" || s == "shift+tab" {
170+
m.focusIndex--
171+
} else {
172+
m.focusIndex++
173+
}
174+
175+
if m.focusIndex > len(m.inputs) {
176+
m.focusIndex = 0
177+
} else if m.focusIndex < 0 {
178+
m.focusIndex = len(m.inputs)
179+
}
180+
181+
cmds := make([]tea.Cmd, len(m.inputs))
182+
for i := 0; i <= len(m.inputs)-1; i++ {
183+
if i == m.focusIndex {
184+
// Set focused state
185+
cmds[i] = m.inputs[i].Focus()
186+
m.inputs[i].PromptStyle = focusedStyle
187+
m.inputs[i].TextStyle = focusedStyle
188+
continue
189+
}
190+
// Remove focused state
191+
m.inputs[i].Blur()
192+
m.inputs[i].PromptStyle = noStyle
193+
m.inputs[i].TextStyle = noStyle
194+
}
195+
196+
return m, tea.Batch(cmds...)
197+
}
198+
}
199+
200+
// Handle character input and blinking
201+
cmd := m.updateInputs(msg)
202+
203+
return m, cmd
204+
}
205+
206+
func (m *model) updateInputs(msg tea.Msg) tea.Cmd {
207+
cmds := make([]tea.Cmd, len(m.inputs))
208+
209+
// Only text inputs with Focus() set will respond, so it's safe to simply
210+
// update all of them here without any further logic.
211+
for i := range m.inputs {
212+
m.inputs[i], cmds[i] = m.inputs[i].Update(msg)
213+
}
214+
215+
return tea.Batch(cmds...)
216+
}
217+
218+
func (m model) View() string {
219+
var b strings.Builder
220+
b.WriteString("It looks like we need a Jira API Token.\n\n")
221+
styledLink := lipgloss.NewStyle().
222+
Bold(true).
223+
Foreground(lipgloss.AdaptiveColor{Light: "4", Dark: "12"}). // Dark Blue or LightBlue
224+
Underline(true).
225+
Render("https://id.atlassian.com/manage/api-tokens")
226+
b.WriteString(fmt.Sprintf(
227+
"First, go to %s to create a personal api token.\n",
228+
styledLink))
229+
230+
for i := range m.inputs {
231+
b.WriteString(m.inputs[i].View())
232+
if i < len(m.inputs)-1 {
233+
b.WriteRune('\n')
234+
}
235+
}
236+
237+
button := &blurredButton
238+
if m.focusIndex == len(m.inputs) {
239+
button = &focusedButton
240+
}
241+
fmt.Fprintf(&b, "\n\n%s\n\n", *button)
242+
243+
b.WriteString(helpStyle.Render("cursor mode is "))
244+
b.WriteString(cursorModeHelpStyle.Render(m.cursorMode.String()))
245+
b.WriteString(helpStyle.Render(" (ctrl+r to change style)"))
246+
247+
return b.String()
248+
}
249+
250+
func BackupConfigFile(filename string) error {
251+
return os.Rename(filename, filename+".bak")
252+
}

cmd/onit.go

+11-6
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,29 @@ package cmd
22

33
import (
44
"fmt"
5-
"github.com/StevenACoffman/jt/pkg/atlassian"
65
"os"
76

7+
"github.com/StevenACoffman/jt/pkg/atlassian"
8+
89
"github.com/spf13/cobra"
910
)
1011

1112
// onitCmd represents the onit command
1213
var onitCmd = &cobra.Command{
1314
Use: "onit",
1415
Short: "Self-assign and transition an issue to In Progress status",
15-
Long: `Assign the issue to yourself and transition an issue to In Progress status`,
16+
Long: `Assign the issue to yourself and transition an issue to In Progress status`,
17+
Args: cobra.RangeArgs(0, 1),
1618
Run: func(cmd *cobra.Command, args []string) {
17-
19+
if jiraConfig == nil {
20+
configure()
21+
}
22+
var issueKey string
1823
if len(args) == 0 {
19-
fmt.Println("You failed to pass a jira issue argument")
20-
os.Exit(exitFail)
24+
issueKey = getIssueFromGitBranch()
25+
} else {
26+
issueKey = args[0]
2127
}
22-
issueKey := args[0]
2328
issue, _, issueErr := jiraClient.Issue.Get(issueKey, nil)
2429
if issueErr != nil {
2530
fmt.Printf("Unable to get Issue %s: %+v", issueKey, issueErr)

0 commit comments

Comments
 (0)