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 = `
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) (string, error) {
112	raw, err := LaunchEditorWithTemplate(repo, messageFilename, bugTitleTemplate)
113
114	if err != nil {
115		return "", err
116	}
117
118	lines := strings.Split(raw, "\n")
119
120	var title string
121	for _, line := range lines {
122		if strings.HasPrefix(line, "#") {
123			continue
124		}
125		trimmed := strings.TrimSpace(line)
126		if trimmed == "" {
127			continue
128		}
129		title = trimmed
130		break
131	}
132
133	if title == "" {
134		return "", ErrEmptyTitle
135	}
136
137	return title, nil
138}
139
140func LaunchEditorWithTemplate(repo repository.Repo, fileName string, template string) (string, error) {
141	path := fmt.Sprintf("%s/.git/%s", repo.GetPath(), fileName)
142
143	err := ioutil.WriteFile(path, []byte(template), 0644)
144
145	if err != nil {
146		return "", err
147	}
148
149	return LaunchEditor(repo, fileName)
150}
151
152// LaunchEditor launches the default editor configured for the given repo. This
153// method blocks until the editor command has returned.
154//
155// The specified filename should be a temporary file and provided as a relative path
156// from the repo (e.g. "FILENAME" will be converted to ".git/FILENAME"). This file
157// will be deleted after the editor is closed and its contents have been read.
158//
159// This method returns the text that was read from the temporary file, or
160// an error if any step in the process failed.
161func LaunchEditor(repo repository.Repo, fileName string) (string, error) {
162	path := fmt.Sprintf("%s/.git/%s", repo.GetPath(), fileName)
163	defer os.Remove(path)
164
165	editor, err := repo.GetCoreEditor()
166	if err != nil {
167		return "", fmt.Errorf("Unable to detect default git editor: %v\n", err)
168	}
169
170	cmd, err := startInlineCommand(editor, path)
171	if err != nil {
172		// Running the editor directly did not work. This might mean that
173		// the editor string is not a path to an executable, but rather
174		// a shell command (e.g. "emacsclient --tty"). As such, we'll try
175		// to run the command through bash, and if that fails, try with sh
176		args := []string{"-c", fmt.Sprintf("%s %q", editor, path)}
177		cmd, err = startInlineCommand("bash", args...)
178		if err != nil {
179			cmd, err = startInlineCommand("sh", args...)
180		}
181	}
182	if err != nil {
183		return "", fmt.Errorf("Unable to start editor: %v\n", err)
184	}
185
186	if err := cmd.Wait(); err != nil {
187		return "", fmt.Errorf("Editing finished with error: %v\n", err)
188	}
189
190	output, err := ioutil.ReadFile(path)
191
192	if err != nil {
193		return "", fmt.Errorf("Error reading edited file: %v\n", err)
194	}
195
196	return string(output), err
197}
198
199// FromFile loads and returns the contents of a given file. If - is passed
200// through, much like git, it will read from stdin. This can be piped data,
201// unless there is a tty in which case the user will be prompted to enter a
202// message.
203func FromFile(fileName string) (string, error) {
204	if fileName == "-" {
205		stat, err := os.Stdin.Stat()
206		if err != nil {
207			return "", fmt.Errorf("Error reading from stdin: %v\n", err)
208		}
209		if (stat.Mode() & os.ModeCharDevice) == 0 {
210			// There is no tty. This will allow us to read piped data instead.
211			output, err := ioutil.ReadAll(os.Stdin)
212			if err != nil {
213				return "", fmt.Errorf("Error reading from stdin: %v\n", err)
214			}
215			return string(output), err
216		}
217
218		fmt.Printf("(reading comment from standard input)\n")
219		var output bytes.Buffer
220		s := bufio.NewScanner(os.Stdin)
221		for s.Scan() {
222			output.Write(s.Bytes())
223			output.WriteRune('\n')
224		}
225		return output.String(), nil
226	}
227
228	output, err := ioutil.ReadFile(fileName)
229	if err != nil {
230		return "", fmt.Errorf("Error reading file: %v\n", err)
231	}
232	return string(output), err
233}
234
235func startInlineCommand(command string, args ...string) (*exec.Cmd, error) {
236	cmd := exec.Command(command, args...)
237	cmd.Stdin = os.Stdin
238	cmd.Stdout = os.Stdout
239	cmd.Stderr = os.Stderr
240	err := cmd.Start()
241	return cmd, err
242}