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 }