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