github.com/driusan/dgit@v0.0.0-20221118233547-f39f0c15edbb/git/commit.go (about) 1 package git 2 3 import ( 4 "fmt" 5 "log" 6 "os" 7 "regexp" 8 "strings" 9 "time" 10 ) 11 12 type CommitOptions struct { 13 All bool 14 Patch bool 15 16 ResetAuthor bool 17 Amend bool 18 19 Date time.Time 20 Author Person 21 22 Signoff bool 23 NoVerify bool 24 AllowEmpty bool 25 AllowEmptyMessage bool 26 27 NoPostRewrite bool 28 Include bool 29 Only bool 30 Quiet bool 31 32 CleanupMode string 33 NoEdit bool 34 35 // Should be passed to CommitTree, which needs support first: 36 // GPGSign GPGKeyID 37 // NoGpgSign bool 38 39 // Things that are used to create the commit message and need to be 40 // parsed by package cmd/, but not included here. 41 // ReuseMessage, ReeditMessage, Fixup, Squash Commitish 42 // File string 43 // Message string (-m) 44 // Template File (COMMIT_EDITMSG) 45 // Status, NoStatus bool 46 // Verbose bool 47 // 48 49 // Things that only affect the output with --dry-run. 50 // Note: Printing the status after --dry-run isn't implemented, 51 // all it does is prevent the call to UpdateRef after CommitTree. 52 // Most of these are a no-op. 53 DryRun bool 54 Short bool 55 Branch bool 56 Porcelain bool 57 Long bool 58 NullTerminate bool 59 UntrackedMode StatusUntrackedMode 60 61 // FIXME: Add all the missing options here. 62 } 63 64 // Commit implements the command "git commit" in the repository pointed 65 // to by c. 66 func Commit(c *Client, opts CommitOptions, message CommitMessage, files []File) (CommitID, error) { 67 if !opts.AllowEmptyMessage && message == "" { 68 return CommitID{}, fmt.Errorf("Aborting commit due to empty commit message.") 69 } 70 if opts.Patch { 71 return CommitID{}, fmt.Errorf("Commit --patch not implemented") 72 } 73 74 var idx *Index 75 76 if opts.All { 77 var tostage []File 78 if opts.Include { 79 tostage = files 80 } 81 82 if _, err := Add(c, AddOptions{Update: true, DryRun: opts.DryRun}, tostage); err != nil { 83 log.Println("Commit adding files:", err) 84 return CommitID{}, err 85 } 86 } 87 88 if !opts.All && len(files) != 0 { 89 var idx1 *Index 90 head, err := c.GetHeadCommit() 91 if err != nil { 92 idx1 = NewIndex() 93 } else { 94 idx1, err = ReadTree(c, ReadTreeOptions{DryRun: true}, head) 95 if err != nil { 96 return CommitID{}, err 97 } 98 } 99 idx2, err := UpdateIndex(c, idx1, UpdateIndexOptions{Add: true, Remove: true}, files) 100 if err != nil { 101 return CommitID{}, err 102 } 103 idx = idx2 104 } else { 105 idx1, err := c.GitDir.ReadIndex() 106 if err != nil { 107 return CommitID{}, err 108 } 109 idx = idx1 110 } 111 // Happy path: write the tree 112 treeid, err := WriteTreeFromIndex(c, idx, WriteTreeOptions{}) 113 if err != nil { 114 return CommitID{}, err 115 } 116 // Write the commit object 117 var parents []CommitID 118 oldHead, err := c.GetHeadCommit() 119 if opts.Amend { 120 parents, err = oldHead.Parents(c) 121 if err != nil { 122 return CommitID{}, err 123 } 124 author, err := oldHead.GetAuthor(c) 125 if err != nil { 126 return CommitID{}, err 127 } 128 if !opts.ResetAuthor { 129 // Back up the environment variables that commit uses to 130 // communicate with commit-tree, so that nothing external 131 // changes to the caller of this script. 132 defer func(oldauthorname, oldauthoremail, oldauthordate string) { 133 os.Setenv("GIT_AUTHOR_NAME", oldauthorname) 134 os.Setenv("GIT_AUTHOR_EMAIL", oldauthoremail) 135 os.Setenv("GIT_AUTHOR_DATE", oldauthordate) 136 137 }( 138 os.Getenv("GIT_AUTHOR_NAME"), 139 os.Getenv("GIT_AUTHOR_EMAIL"), 140 os.Getenv("GIT_AUTHOR_DATE"), 141 ) 142 os.Setenv("GIT_AUTHOR_NAME", author.Name) 143 os.Setenv("GIT_AUTHOR_EMAIL", author.Email) 144 date, err := oldHead.GetDate(c) 145 if err != nil { 146 return CommitID{}, err 147 } 148 os.Setenv("GIT_AUTHOR_DATE", date.Format("Mon, 02 Jan 2006 15:04:05 -0700")) 149 } 150 goto skipemptycheck 151 } else if err == nil || err == DetachedHead { 152 parents = append(parents, oldHead) 153 } 154 155 if !opts.AllowEmpty { 156 if oldtree, err := oldHead.TreeID(c); err == nil { 157 if oldtree == treeid { 158 return CommitID{}, fmt.Errorf("No changes staged for commit.") 159 } 160 } 161 } 162 skipemptycheck: 163 cleanMessage, err := message.Cleanup(opts.CleanupMode, !opts.NoEdit) 164 if err != nil { 165 return CommitID{}, err 166 } 167 var noConfig error 168 cid, err := CommitTree(c, CommitTreeOptions{}, TreeID(treeid), parents, cleanMessage) 169 switch err { 170 case nil: 171 // Nothing 172 case NoGlobalConfig: 173 noConfig = err 174 default: 175 return CommitID{}, err 176 } 177 178 // Update the reference 179 var refmsg string 180 if len(cleanMessage) < 50 { 181 refmsg = cleanMessage 182 } else { 183 refmsg = cleanMessage[:50] 184 } 185 refmsg = fmt.Sprintf("commit: %s (dgit)", refmsg) 186 187 if err := UpdateRef(c, UpdateRefOptions{OldValue: oldHead, CreateReflog: true}, "HEAD", cid, refmsg); err != nil { 188 return CommitID{}, err 189 } 190 return cid, noConfig 191 } 192 193 type CommitMessage string 194 195 func (cm CommitMessage) String() string { 196 return string(cm) 197 } 198 func (cm CommitMessage) Cleanup(mode string, edit bool) (string, error) { 199 switch mode { 200 case "strip": 201 return cm.strip(), nil 202 case "whitespace": 203 return cm.whitespace(), nil 204 case "", "default": 205 if edit { 206 return cm.strip(), nil 207 } 208 return cm.whitespace(), nil 209 case "scissors": 210 return string(cm), fmt.Errorf("Unsupported cleanup mode") 211 default: 212 return string(cm), fmt.Errorf("Invalid cleanup mode") 213 } 214 } 215 216 func (cm CommitMessage) whitespace() string { 217 nonewlineRE, err := regexp.Compile("([\n]+)\n") 218 if err != nil { 219 panic(err) 220 } 221 replaced := nonewlineRE.ReplaceAllString(string(cm), "\n\n") 222 return strings.TrimSpace(replaced) + "\n" 223 } 224 225 func (cm CommitMessage) strip() string { 226 lines := strings.Split(cm.whitespace(), "\n") 227 filtered := make([]string, 0, len(lines)) 228 for _, line := range lines { 229 if len(line) >= 1 && line[0] == '#' { 230 continue 231 } 232 filtered = append(filtered, line) 233 } 234 return strings.Join(filtered, "\n") 235 } 236 237 func (cm CommitMessage) Subject() string { 238 lines := strings.SplitN(cm.whitespace(), "\n", 1) 239 if len(lines) > 0 { 240 return strings.TrimSpace(lines[0]) 241 } 242 return "" 243 }