input.go

  1// Inspired by the git-appraise project
  2
  3// Package input contains helpers to use a text editor as an input for
  4// various field of a bug
  5package input
  6
  7import (
  8	"bufio"
  9	"bytes"
 10	"fmt"
 11	"io"
 12	"os"
 13	"os/exec"
 14	"path/filepath"
 15
 16	"github.com/go-git/go-billy/v5/util"
 17
 18	"github.com/MichaelMure/git-bug/repository"
 19)
 20
 21// LaunchEditorWithTemplate will launch an editor as LaunchEditor do, but with a
 22// provided template.
 23func LaunchEditorWithTemplate(repo repository.RepoCommonStorage, fileName string, template string) (string, error) {
 24	err := util.WriteFile(repo.LocalStorage(), fileName, []byte(template), 0644)
 25	if err != nil {
 26		return "", err
 27	}
 28
 29	return LaunchEditor(repo, fileName)
 30}
 31
 32// LaunchEditor launches the default editor configured for the given repo. This
 33// method blocks until the editor command has returned.
 34//
 35// The specified filename should be a temporary file and provided as a relative path
 36// from the repo (e.g. "FILENAME" will be converted to "[<reporoot>/].git/git-bug/FILENAME"). This file
 37// will be deleted after the editor is closed and its contents have been read.
 38//
 39// This method returns the text that was read from the temporary file, or
 40// an error if any step in the process failed.
 41func LaunchEditor(repo repository.RepoCommonStorage, fileName string) (string, error) {
 42	defer repo.LocalStorage().Remove(fileName)
 43
 44	editor, err := repo.GetCoreEditor()
 45	if err != nil {
 46		return "", fmt.Errorf("Unable to detect default git editor: %v\n", err)
 47	}
 48
 49	repo.LocalStorage().Root()
 50
 51	// bypass the interface but that's ok: we need that because we are communicating
 52	// the absolute path to an external program
 53	path := filepath.Join(repo.LocalStorage().Root(), fileName)
 54
 55	cmd, err := startInlineCommand(editor, path)
 56	if err != nil {
 57		// Running the editor directly did not work. This might mean that
 58		// the editor string is not a path to an executable, but rather
 59		// a shell command (e.g. "emacsclient --tty"). As such, we'll try
 60		// to run the command through bash, and if that fails, try with sh
 61		args := []string{"-c", fmt.Sprintf("%s %q", editor, path)}
 62		cmd, err = startInlineCommand("bash", args...)
 63		if err != nil {
 64			cmd, err = startInlineCommand("sh", args...)
 65		}
 66	}
 67	if err != nil {
 68		return "", fmt.Errorf("Unable to start editor: %v\n", err)
 69	}
 70
 71	if err := cmd.Wait(); err != nil {
 72		return "", fmt.Errorf("Editing finished with error: %v\n", err)
 73	}
 74
 75	output, err := os.ReadFile(path)
 76
 77	if err != nil {
 78		return "", fmt.Errorf("Error reading edited file: %v\n", err)
 79	}
 80
 81	return string(output), err
 82}
 83
 84// FromFile loads and returns the contents of a given file. If - is passed
 85// through, much like git, it will read from stdin. This can be piped data,
 86// unless there is a tty in which case the user will be prompted to enter a
 87// message.
 88func FromFile(fileName string) (string, error) {
 89	if fileName == "-" {
 90		stat, err := os.Stdin.Stat()
 91		if err != nil {
 92			return "", fmt.Errorf("Error reading from stdin: %v\n", err)
 93		}
 94		if (stat.Mode() & os.ModeCharDevice) == 0 {
 95			// There is no tty. This will allow us to read piped data instead.
 96			output, err := io.ReadAll(os.Stdin)
 97			if err != nil {
 98				return "", fmt.Errorf("Error reading from stdin: %v\n", err)
 99			}
100			return string(output), err
101		}
102
103		fmt.Printf("(reading comment from standard input)\n")
104		var output bytes.Buffer
105		s := bufio.NewScanner(os.Stdin)
106		for s.Scan() {
107			output.Write(s.Bytes())
108			output.WriteRune('\n')
109		}
110		return output.String(), nil
111	}
112
113	output, err := os.ReadFile(fileName)
114	if err != nil {
115		return "", fmt.Errorf("Error reading file: %v\n", err)
116	}
117	return string(output), err
118}
119
120func startInlineCommand(command string, args ...string) (*exec.Cmd, error) {
121	cmd := exec.Command(command, args...)
122	cmd.Stdin = os.Stdin
123	cmd.Stdout = os.Stdout
124	cmd.Stderr = os.Stderr
125	err := cmd.Start()
126	return cmd, err
127}