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
17const messageFilename = "BUG_MESSAGE_EDITMSG"
18
19// ErrEmptyMessage is returned when the required message has not been entered
20var ErrEmptyMessage = errors.New("empty message")
21
22// ErrEmptyMessage is returned when the required title has not been entered
23var ErrEmptyTitle = errors.New("empty title")
24
25const bugTitleCommentTemplate = `%s%s
26
27# Please enter the title and comment message. The first non-empty line will be
28# used as the title. Lines starting with '#' will be ignored.
29# An empty title aborts the operation.
30`
31
32// BugCreateEditorInput will open the default editor in the terminal with a
33// template for the user to fill. The file is then processed to extract title
34// and message.
35func BugCreateEditorInput(repo repository.Repo, preTitle string, preMessage string) (string, string, error) {
36 if preMessage != "" {
37 preMessage = "\n\n" + preMessage
38 }
39
40 template := fmt.Sprintf(bugTitleCommentTemplate, preTitle, preMessage)
41
42 raw, err := launchEditorWithTemplate(repo, messageFilename, template)
43
44 if err != nil {
45 return "", "", err
46 }
47
48 lines := strings.Split(raw, "\n")
49
50 var title string
51 var buffer bytes.Buffer
52 for _, line := range lines {
53 if strings.HasPrefix(line, "#") {
54 continue
55 }
56
57 if title == "" {
58 trimmed := strings.TrimSpace(line)
59 if trimmed != "" {
60 title = trimmed
61 }
62 continue
63 }
64
65 buffer.WriteString(line)
66 buffer.WriteString("\n")
67 }
68
69 if title == "" {
70 return "", "", ErrEmptyTitle
71 }
72
73 message := strings.TrimSpace(buffer.String())
74
75 return title, message, nil
76}
77
78const bugCommentTemplate = `
79
80# Please enter the comment message. Lines starting with '#' will be ignored,
81# and an empty message aborts the operation.
82`
83
84// BugCommentEditorInput will open the default editor in the terminal with a
85// template for the user to fill. The file is then processed to extract a comment.
86func BugCommentEditorInput(repo repository.Repo) (string, error) {
87 raw, err := launchEditorWithTemplate(repo, messageFilename, bugCommentTemplate)
88
89 if err != nil {
90 return "", err
91 }
92
93 lines := strings.Split(raw, "\n")
94
95 var buffer bytes.Buffer
96 for _, line := range lines {
97 if strings.HasPrefix(line, "#") {
98 continue
99 }
100 buffer.WriteString(line)
101 buffer.WriteString("\n")
102 }
103
104 message := strings.TrimSpace(buffer.String())
105
106 if message == "" {
107 return "", ErrEmptyMessage
108 }
109
110 return message, nil
111}
112
113const bugTitleTemplate = `%s
114
115# Please enter the new title. Only one line will used.
116# Lines starting with '#' will be ignored, and an empty title aborts the operation.
117`
118
119// BugTitleEditorInput will open the default editor in the terminal with a
120// template for the user to fill. The file is then processed to extract a title.
121func BugTitleEditorInput(repo repository.Repo, preTitle string) (string, error) {
122 template := fmt.Sprintf(bugTitleTemplate, preTitle)
123 raw, err := launchEditorWithTemplate(repo, messageFilename, template)
124
125 if err != nil {
126 return "", err
127 }
128
129 lines := strings.Split(raw, "\n")
130
131 var title string
132 for _, line := range lines {
133 if strings.HasPrefix(line, "#") {
134 continue
135 }
136 trimmed := strings.TrimSpace(line)
137 if trimmed == "" {
138 continue
139 }
140 title = trimmed
141 break
142 }
143
144 if title == "" {
145 return "", ErrEmptyTitle
146 }
147
148 return title, nil
149}
150
151// launchEditorWithTemplate will launch an editor as launchEditor do, but with a
152// provided template.
153func launchEditorWithTemplate(repo repository.Repo, fileName string, template string) (string, error) {
154 path := fmt.Sprintf("%s/.git/%s", repo.GetPath(), fileName)
155
156 err := ioutil.WriteFile(path, []byte(template), 0644)
157
158 if err != nil {
159 return "", err
160 }
161
162 return launchEditor(repo, fileName)
163}
164
165// launchEditor launches the default editor configured for the given repo. This
166// method blocks until the editor command has returned.
167//
168// The specified filename should be a temporary file and provided as a relative path
169// from the repo (e.g. "FILENAME" will be converted to ".git/FILENAME"). This file
170// will be deleted after the editor is closed and its contents have been read.
171//
172// This method returns the text that was read from the temporary file, or
173// an error if any step in the process failed.
174func launchEditor(repo repository.Repo, fileName string) (string, error) {
175 path := fmt.Sprintf("%s/.git/%s", repo.GetPath(), fileName)
176 defer os.Remove(path)
177
178 editor, err := repo.GetCoreEditor()
179 if err != nil {
180 return "", fmt.Errorf("Unable to detect default git editor: %v\n", err)
181 }
182
183 cmd, err := startInlineCommand(editor, path)
184 if err != nil {
185 // Running the editor directly did not work. This might mean that
186 // the editor string is not a path to an executable, but rather
187 // a shell command (e.g. "emacsclient --tty"). As such, we'll try
188 // to run the command through bash, and if that fails, try with sh
189 args := []string{"-c", fmt.Sprintf("%s %q", editor, path)}
190 cmd, err = startInlineCommand("bash", args...)
191 if err != nil {
192 cmd, err = startInlineCommand("sh", args...)
193 }
194 }
195 if err != nil {
196 return "", fmt.Errorf("Unable to start editor: %v\n", err)
197 }
198
199 if err := cmd.Wait(); err != nil {
200 return "", fmt.Errorf("Editing finished with error: %v\n", err)
201 }
202
203 output, err := ioutil.ReadFile(path)
204
205 if err != nil {
206 return "", fmt.Errorf("Error reading edited file: %v\n", err)
207 }
208
209 return string(output), err
210}
211
212// FromFile loads and returns the contents of a given file. If - is passed
213// through, much like git, it will read from stdin. This can be piped data,
214// unless there is a tty in which case the user will be prompted to enter a
215// message.
216func FromFile(fileName string) (string, error) {
217 if fileName == "-" {
218 stat, err := os.Stdin.Stat()
219 if err != nil {
220 return "", fmt.Errorf("Error reading from stdin: %v\n", err)
221 }
222 if (stat.Mode() & os.ModeCharDevice) == 0 {
223 // There is no tty. This will allow us to read piped data instead.
224 output, err := ioutil.ReadAll(os.Stdin)
225 if err != nil {
226 return "", fmt.Errorf("Error reading from stdin: %v\n", err)
227 }
228 return string(output), err
229 }
230
231 fmt.Printf("(reading comment from standard input)\n")
232 var output bytes.Buffer
233 s := bufio.NewScanner(os.Stdin)
234 for s.Scan() {
235 output.Write(s.Bytes())
236 output.WriteRune('\n')
237 }
238 return output.String(), nil
239 }
240
241 output, err := ioutil.ReadFile(fileName)
242 if err != nil {
243 return "", fmt.Errorf("Error reading file: %v\n", err)
244 }
245 return string(output), err
246}
247
248func startInlineCommand(command string, args ...string) (*exec.Cmd, error) {
249 cmd := exec.Command(command, args...)
250 cmd.Stdin = os.Stdin
251 cmd.Stdout = os.Stdout
252 cmd.Stderr = os.Stderr
253 err := cmd.Start()
254 return cmd, err
255}