fork of go-git with some jj specific features
at v5.13.1 7.3 kB view raw
1package git 2 3import ( 4 "bytes" 5 "errors" 6 "io" 7 "path" 8 "regexp" 9 "sort" 10 "strings" 11 12 "github.com/go-git/go-git/v5/plumbing" 13 "github.com/go-git/go-git/v5/plumbing/filemode" 14 "github.com/go-git/go-git/v5/plumbing/format/index" 15 "github.com/go-git/go-git/v5/plumbing/object" 16 "github.com/go-git/go-git/v5/storage" 17 18 "github.com/ProtonMail/go-crypto/openpgp" 19 "github.com/ProtonMail/go-crypto/openpgp/packet" 20 "github.com/go-git/go-billy/v5" 21) 22 23var ( 24 // ErrEmptyCommit occurs when a commit is attempted using a clean 25 // working tree, with no changes to be committed. 26 ErrEmptyCommit = errors.New("cannot create empty commit: clean working tree") 27 28 // characters to be removed from user name and/or email before using them to build a commit object 29 // See https://git-scm.com/docs/git-commit#_commit_information 30 invalidCharactersRe = regexp.MustCompile(`[<>\n]`) 31) 32 33// Commit stores the current contents of the index in a new commit along with 34// a log message from the user describing the changes. 35func (w *Worktree) Commit(msg string, opts *CommitOptions) (plumbing.Hash, error) { 36 if err := opts.Validate(w.r); err != nil { 37 return plumbing.ZeroHash, err 38 } 39 40 if opts.All { 41 if err := w.autoAddModifiedAndDeleted(); err != nil { 42 return plumbing.ZeroHash, err 43 } 44 } 45 46 if opts.Amend { 47 head, err := w.r.Head() 48 if err != nil { 49 return plumbing.ZeroHash, err 50 } 51 headCommit, err := w.r.CommitObject(head.Hash()) 52 if err != nil { 53 return plumbing.ZeroHash, err 54 } 55 56 opts.Parents = nil 57 if len(headCommit.ParentHashes) != 0 { 58 opts.Parents = []plumbing.Hash{headCommit.ParentHashes[0]} 59 } 60 } 61 62 idx, err := w.r.Storer.Index() 63 if err != nil { 64 return plumbing.ZeroHash, err 65 } 66 67 // First handle the case of the first commit in the repository being empty. 68 if len(opts.Parents) == 0 && len(idx.Entries) == 0 && !opts.AllowEmptyCommits { 69 return plumbing.ZeroHash, ErrEmptyCommit 70 } 71 72 h := &buildTreeHelper{ 73 fs: w.Filesystem, 74 s: w.r.Storer, 75 } 76 77 treeHash, err := h.BuildTree(idx, opts) 78 if err != nil { 79 return plumbing.ZeroHash, err 80 } 81 82 previousTree := plumbing.ZeroHash 83 if len(opts.Parents) > 0 { 84 parentCommit, err := w.r.CommitObject(opts.Parents[0]) 85 if err != nil { 86 return plumbing.ZeroHash, err 87 } 88 previousTree = parentCommit.TreeHash 89 } 90 91 if treeHash == previousTree && !opts.AllowEmptyCommits { 92 return plumbing.ZeroHash, ErrEmptyCommit 93 } 94 95 commit, err := w.buildCommitObject(msg, opts, treeHash) 96 if err != nil { 97 return plumbing.ZeroHash, err 98 } 99 100 return commit, w.updateHEAD(commit) 101} 102 103func (w *Worktree) autoAddModifiedAndDeleted() error { 104 s, err := w.Status() 105 if err != nil { 106 return err 107 } 108 109 idx, err := w.r.Storer.Index() 110 if err != nil { 111 return err 112 } 113 114 for path, fs := range s { 115 if fs.Worktree != Modified && fs.Worktree != Deleted { 116 continue 117 } 118 119 if _, _, err := w.doAddFile(idx, s, path, nil); err != nil { 120 return err 121 } 122 123 } 124 125 return w.r.Storer.SetIndex(idx) 126} 127 128func (w *Worktree) updateHEAD(commit plumbing.Hash) error { 129 head, err := w.r.Storer.Reference(plumbing.HEAD) 130 if err != nil { 131 return err 132 } 133 134 name := plumbing.HEAD 135 if head.Type() != plumbing.HashReference { 136 name = head.Target() 137 } 138 139 ref := plumbing.NewHashReference(name, commit) 140 return w.r.Storer.SetReference(ref) 141} 142 143func (w *Worktree) buildCommitObject(msg string, opts *CommitOptions, tree plumbing.Hash) (plumbing.Hash, error) { 144 commit := &object.Commit{ 145 Author: w.sanitize(*opts.Author), 146 Committer: w.sanitize(*opts.Committer), 147 Message: msg, 148 TreeHash: tree, 149 ParentHashes: opts.Parents, 150 } 151 152 // Convert SignKey into a Signer if set. Existing Signer should take priority. 153 signer := opts.Signer 154 if signer == nil && opts.SignKey != nil { 155 signer = &gpgSigner{key: opts.SignKey} 156 } 157 if signer != nil { 158 sig, err := signObject(signer, commit) 159 if err != nil { 160 return plumbing.ZeroHash, err 161 } 162 commit.PGPSignature = string(sig) 163 } 164 165 obj := w.r.Storer.NewEncodedObject() 166 if err := commit.Encode(obj); err != nil { 167 return plumbing.ZeroHash, err 168 } 169 return w.r.Storer.SetEncodedObject(obj) 170} 171 172func (w *Worktree) sanitize(signature object.Signature) object.Signature { 173 return object.Signature{ 174 Name: invalidCharactersRe.ReplaceAllString(signature.Name, ""), 175 Email: invalidCharactersRe.ReplaceAllString(signature.Email, ""), 176 When: signature.When, 177 } 178} 179 180type gpgSigner struct { 181 key *openpgp.Entity 182 cfg *packet.Config 183} 184 185func (s *gpgSigner) Sign(message io.Reader) ([]byte, error) { 186 var b bytes.Buffer 187 if err := openpgp.ArmoredDetachSign(&b, s.key, message, s.cfg); err != nil { 188 return nil, err 189 } 190 return b.Bytes(), nil 191} 192 193// buildTreeHelper converts a given index.Index file into multiple git objects 194// reading the blobs from the given filesystem and creating the trees from the 195// index structure. The created objects are pushed to a given Storer. 196type buildTreeHelper struct { 197 fs billy.Filesystem 198 s storage.Storer 199 200 trees map[string]*object.Tree 201 entries map[string]*object.TreeEntry 202} 203 204// BuildTree builds the tree objects and push its to the storer, the hash 205// of the root tree is returned. 206func (h *buildTreeHelper) BuildTree(idx *index.Index, opts *CommitOptions) (plumbing.Hash, error) { 207 const rootNode = "" 208 h.trees = map[string]*object.Tree{rootNode: {}} 209 h.entries = map[string]*object.TreeEntry{} 210 211 for _, e := range idx.Entries { 212 if err := h.commitIndexEntry(e); err != nil { 213 return plumbing.ZeroHash, err 214 } 215 } 216 217 return h.copyTreeToStorageRecursive(rootNode, h.trees[rootNode]) 218} 219 220func (h *buildTreeHelper) commitIndexEntry(e *index.Entry) error { 221 parts := strings.Split(e.Name, "/") 222 223 var fullpath string 224 for _, part := range parts { 225 parent := fullpath 226 fullpath = path.Join(fullpath, part) 227 228 h.doBuildTree(e, parent, fullpath) 229 } 230 231 return nil 232} 233 234func (h *buildTreeHelper) doBuildTree(e *index.Entry, parent, fullpath string) { 235 if _, ok := h.trees[fullpath]; ok { 236 return 237 } 238 239 if _, ok := h.entries[fullpath]; ok { 240 return 241 } 242 243 te := object.TreeEntry{Name: path.Base(fullpath)} 244 245 if fullpath == e.Name { 246 te.Mode = e.Mode 247 te.Hash = e.Hash 248 } else { 249 te.Mode = filemode.Dir 250 h.trees[fullpath] = &object.Tree{} 251 } 252 253 h.trees[parent].Entries = append(h.trees[parent].Entries, te) 254} 255 256type sortableEntries []object.TreeEntry 257 258func (sortableEntries) sortName(te object.TreeEntry) string { 259 if te.Mode == filemode.Dir { 260 return te.Name + "/" 261 } 262 return te.Name 263} 264func (se sortableEntries) Len() int { return len(se) } 265func (se sortableEntries) Less(i int, j int) bool { return se.sortName(se[i]) < se.sortName(se[j]) } 266func (se sortableEntries) Swap(i int, j int) { se[i], se[j] = se[j], se[i] } 267 268func (h *buildTreeHelper) copyTreeToStorageRecursive(parent string, t *object.Tree) (plumbing.Hash, error) { 269 sort.Sort(sortableEntries(t.Entries)) 270 for i, e := range t.Entries { 271 if e.Mode != filemode.Dir && !e.Hash.IsZero() { 272 continue 273 } 274 275 path := path.Join(parent, e.Name) 276 277 var err error 278 e.Hash, err = h.copyTreeToStorageRecursive(path, h.trees[path]) 279 if err != nil { 280 return plumbing.ZeroHash, err 281 } 282 283 t.Entries[i] = e 284 } 285 286 o := h.s.NewEncodedObject() 287 if err := t.Encode(o); err != nil { 288 return plumbing.ZeroHash, err 289 } 290 291 hash := o.Hash() 292 if h.s.HasEncodedObject(hash) == nil { 293 return hash, nil 294 } 295 return h.s.SetEncodedObject(o) 296}