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