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