input.go

  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}