input.go

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