github.com/zaquestion/lab@v0.25.1/internal/git/edit.go (about)

     1  package git
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"os/exec"
     8  	"path/filepath"
     9  	"regexp"
    10  	"strings"
    11  
    12  	"github.com/google/shlex"
    13  )
    14  
    15  // Edit opens a file in the users editor and returns the title and body.
    16  func Edit(filePrefix, message string) (string, string, error) {
    17  	contents, err := EditFile(filePrefix, message)
    18  	if err != nil {
    19  		return "", "", err
    20  	}
    21  
    22  	return ParseTitleBody(strings.TrimSpace(string(contents)))
    23  }
    24  
    25  // EditFile opens a file in the users editor and returns the contents. It
    26  // stores a temporary file in your .git directory or /tmp if accessed outside of
    27  // a git repo.
    28  func EditFile(filePrefix, message string) (string, error) {
    29  	var (
    30  		dir string
    31  		err error
    32  	)
    33  	if InsideGitRepo() {
    34  		dir, err = Dir()
    35  		if err != nil {
    36  			return "", err
    37  		}
    38  	} else {
    39  		dir = "/tmp"
    40  	}
    41  	filePath := filepath.Join(dir, fmt.Sprintf("%s_EDITMSG", filePrefix))
    42  	editor, err := editor()
    43  	if err != nil {
    44  		return "", err
    45  	}
    46  
    47  	// Write generated/template message to file
    48  	if _, err := os.Stat(filePath); os.IsNotExist(err) && message != "" {
    49  		err = ioutil.WriteFile(filePath, []byte(message), 0644)
    50  		if err != nil {
    51  			fmt.Printf("ERROR(WriteFile): Saved file contents written to %s\n", filePath)
    52  			return "", err
    53  		}
    54  	}
    55  
    56  	cmd, err := editorCMD(editor, filePath)
    57  	if err != nil {
    58  		fmt.Printf("ERROR(editorCMD): failed to get editor command \"%s\"\n", editor)
    59  		return "", err
    60  	}
    61  
    62  	err = cmd.Run()
    63  	if err != nil {
    64  		fmt.Printf("ERROR(cmd): Saved file contents written to %s\n", filePath)
    65  		return "", err
    66  	}
    67  
    68  	contents, err := ioutil.ReadFile(filePath)
    69  	if err != nil {
    70  		fmt.Printf("ERROR(ReadFile): Saved file contents written to %s\n", filePath)
    71  		return "", err
    72  	}
    73  
    74  	os.Remove(filePath)
    75  	return removeComments(string(contents))
    76  }
    77  
    78  func editor() (string, error) {
    79  	cmd := New("var", "GIT_EDITOR")
    80  	cmd.Stdout = nil
    81  	a, err := cmd.Output()
    82  	if err != nil {
    83  		return "", err
    84  	}
    85  	editor := strings.TrimSpace(string(a))
    86  	if editor == "" {
    87  		cmd = New("config", "--get", "core.editor")
    88  		cmd.Stdout = nil
    89  		b, err := cmd.Output()
    90  		if err != nil {
    91  			return "", err
    92  		}
    93  		editor = strings.TrimSpace(string(b))
    94  	}
    95  	return editor, nil
    96  }
    97  
    98  func editorCMD(editor, filePath string) (*exec.Cmd, error) {
    99  	// make 'vi' the default editor to avoid empty editor configs
   100  	if editor == "" {
   101  		editor = "vi"
   102  	}
   103  
   104  	r := regexp.MustCompile("[nmg]?vi[m]?$")
   105  	args := make([]string, 0, 3)
   106  	if r.MatchString(editor) {
   107  		args = append(args, "--cmd", "set ft=gitcommit tw=0 wrap lbr")
   108  	}
   109  
   110  	// Split editor command using shell rules for quoting and commenting
   111  	parts, err := shlex.Split(editor)
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  
   116  	name := parts[0]
   117  	if len(parts) > 0 {
   118  		for _, arg := range parts[1:] {
   119  			arg = strings.Replace(arg, "'", "\"", -1)
   120  			args = append(args, arg)
   121  		}
   122  	}
   123  	args = append(args, filePath)
   124  
   125  	cmd := exec.Command(name, args...)
   126  	cmd.Stdin = os.Stdin
   127  	cmd.Stdout = os.Stdout
   128  	cmd.Stderr = os.Stderr
   129  	return cmd, nil
   130  }
   131  
   132  func removeComments(message string) (string, error) {
   133  	// Grab all the lines that don't start with the comment char
   134  	cc := CommentChar()
   135  	r := regexp.MustCompile(`(?m:^)[^` + cc + `].*(?m:$)`)
   136  	cr := regexp.MustCompile(`(?m:^)\s*` + cc)
   137  	parts := r.FindAllString(message, -1)
   138  	noComments := make([]string, 0)
   139  	for _, p := range parts {
   140  		if !cr.MatchString(p) {
   141  			noComments = append(noComments, p)
   142  		}
   143  	}
   144  	return strings.TrimSpace(strings.Join(noComments, "\n")), nil
   145  }
   146  
   147  func ParseTitleBody(message string) (string, string, error) {
   148  	msg, err := removeComments(message)
   149  	if err != nil {
   150  		return "", "", err
   151  	}
   152  
   153  	if msg == "" {
   154  		return "", "", nil
   155  	}
   156  
   157  	r := regexp.MustCompile(`\n\s*\n`)
   158  	msg = strings.Replace(msg, "\\#", "#", -1)
   159  	parts := r.Split(msg, 2)
   160  
   161  	if strings.Contains(parts[0], "\n") {
   162  		return "\n", parts[0], nil
   163  	}
   164  	if len(parts) < 2 {
   165  		return parts[0], "", nil
   166  	}
   167  	return parts[0], parts[1], nil
   168  }