1// Originally taken from the git-appraise project
2
3package input
4
5import (
6 "bufio"
7 "bytes"
8 "fmt"
9 "github.com/MichaelMure/git-bug/repository"
10 "github.com/pkg/errors"
11 "io/ioutil"
12 "os"
13 "os/exec"
14 "strings"
15)
16
17var ErrEmptyMessage = errors.New("empty message")
18var ErrEmptyTitle = errors.New("empty title")
19
20const bugTitleCommentTemplate = `%s%s
21
22# Please enter the title and comment message. The first non-empty line will be
23# used as the title. Lines starting with '#' will be ignored.
24# An empty title aborts the operation.
25`
26
27func BugCreateEditorInput(repo repository.Repo, fileName string, preTitle string, preMessage string) (string, string, error) {
28 if preMessage != "" {
29 preMessage = "\n\n" + preMessage
30 }
31
32 template := fmt.Sprintf(bugTitleCommentTemplate, preTitle, preMessage)
33
34 raw, err := LaunchEditorWithTemplate(repo, fileName, template)
35
36 if err != nil {
37 return "", "", err
38 }
39
40 lines := strings.Split(raw, "\n")
41
42 var title string
43 var b strings.Builder
44 for _, line := range lines {
45 if strings.HasPrefix(line, "#") {
46 continue
47 }
48
49 if title == "" {
50 trimmed := strings.TrimSpace(line)
51 if trimmed != "" {
52 title = trimmed
53 }
54 continue
55 }
56
57 b.WriteString(line)
58 b.WriteString("\n")
59 }
60
61 if title == "" {
62 return "", "", ErrEmptyTitle
63 }
64
65 message := strings.TrimSpace(b.String())
66
67 return title, message, nil
68}
69
70const bugCommentTemplate = `
71
72# Please enter the comment message. Lines starting with '#' will be ignored,
73# and an empty message aborts the operation.
74`
75
76func BugCommentEditorInput(repo repository.Repo, fileName string) (string, error) {
77 raw, err := LaunchEditorWithTemplate(repo, fileName, bugCommentTemplate)
78
79 if err != nil {
80 return "", err
81 }
82
83 lines := strings.Split(raw, "\n")
84
85 var b strings.Builder
86 for _, line := range lines {
87 if strings.HasPrefix(line, "#") {
88 continue
89 }
90 b.WriteString(line)
91 b.WriteString("\n")
92 }
93
94 message := strings.TrimSpace(b.String())
95
96 if message == "" {
97 return "", ErrEmptyMessage
98 }
99
100 return message, nil
101}
102
103func LaunchEditorWithTemplate(repo repository.Repo, fileName string, template string) (string, error) {
104 path := fmt.Sprintf("%s/.git/%s", repo.GetPath(), fileName)
105
106 err := ioutil.WriteFile(path, []byte(template), 0644)
107
108 if err != nil {
109 return "", err
110 }
111
112 return LaunchEditor(repo, fileName)
113}
114
115// LaunchEditor launches the default editor configured for the given repo. This
116// method blocks until the editor command has returned.
117//
118// The specified filename should be a temporary file and provided as a relative path
119// from the repo (e.g. "FILENAME" will be converted to ".git/FILENAME"). This file
120// will be deleted after the editor is closed and its contents have been read.
121//
122// This method returns the text that was read from the temporary file, or
123// an error if any step in the process failed.
124func LaunchEditor(repo repository.Repo, fileName string) (string, error) {
125 path := fmt.Sprintf("%s/.git/%s", repo.GetPath(), fileName)
126 defer os.Remove(path)
127
128 editor, err := repo.GetCoreEditor()
129 if err != nil {
130 return "", fmt.Errorf("Unable to detect default git editor: %v\n", err)
131 }
132
133 cmd, err := startInlineCommand(editor, path)
134 if err != nil {
135 // Running the editor directly did not work. This might mean that
136 // the editor string is not a path to an executable, but rather
137 // a shell command (e.g. "emacsclient --tty"). As such, we'll try
138 // to run the command through bash, and if that fails, try with sh
139 args := []string{"-c", fmt.Sprintf("%s %q", editor, path)}
140 cmd, err = startInlineCommand("bash", args...)
141 if err != nil {
142 cmd, err = startInlineCommand("sh", args...)
143 }
144 }
145 if err != nil {
146 return "", fmt.Errorf("Unable to start editor: %v\n", err)
147 }
148
149 if err := cmd.Wait(); err != nil {
150 return "", fmt.Errorf("Editing finished with error: %v\n", err)
151 }
152
153 output, err := ioutil.ReadFile(path)
154
155 if err != nil {
156 return "", fmt.Errorf("Error reading edited file: %v\n", err)
157 }
158
159 return string(output), err
160}
161
162// FromFile loads and returns the contents of a given file. If - is passed
163// through, much like git, it will read from stdin. This can be piped data,
164// unless there is a tty in which case the user will be prompted to enter a
165// message.
166func FromFile(fileName string) (string, error) {
167 if fileName == "-" {
168 stat, err := os.Stdin.Stat()
169 if err != nil {
170 return "", fmt.Errorf("Error reading from stdin: %v\n", err)
171 }
172 if (stat.Mode() & os.ModeCharDevice) == 0 {
173 // There is no tty. This will allow us to read piped data instead.
174 output, err := ioutil.ReadAll(os.Stdin)
175 if err != nil {
176 return "", fmt.Errorf("Error reading from stdin: %v\n", err)
177 }
178 return string(output), err
179 }
180
181 fmt.Printf("(reading comment from standard input)\n")
182 var output bytes.Buffer
183 s := bufio.NewScanner(os.Stdin)
184 for s.Scan() {
185 output.Write(s.Bytes())
186 output.WriteRune('\n')
187 }
188 return output.String(), nil
189 }
190
191 output, err := ioutil.ReadFile(fileName)
192 if err != nil {
193 return "", fmt.Errorf("Error reading file: %v\n", err)
194 }
195 return string(output), err
196}
197
198func startInlineCommand(command string, args ...string) (*exec.Cmd, error) {
199 cmd := exec.Command(command, args...)
200 cmd.Stdin = os.Stdin
201 cmd.Stdout = os.Stdout
202 cmd.Stderr = os.Stderr
203 err := cmd.Start()
204 return cmd, err
205}