fork of go-git with some jj specific features
at v5.8.0 5.9 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 idx, err := w.r.Storer.Index() 40 if err != nil { 41 return plumbing.ZeroHash, err 42 } 43 44 h := &buildTreeHelper{ 45 fs: w.Filesystem, 46 s: w.r.Storer, 47 } 48 49 tree, err := h.BuildTree(idx, opts) 50 if err != nil { 51 return plumbing.ZeroHash, err 52 } 53 54 commit, err := w.buildCommitObject(msg, opts, tree) 55 if err != nil { 56 return plumbing.ZeroHash, err 57 } 58 59 return commit, w.updateHEAD(commit) 60} 61 62func (w *Worktree) autoAddModifiedAndDeleted() error { 63 s, err := w.Status() 64 if err != nil { 65 return err 66 } 67 68 idx, err := w.r.Storer.Index() 69 if err != nil { 70 return err 71 } 72 73 for path, fs := range s { 74 if fs.Worktree != Modified && fs.Worktree != Deleted { 75 continue 76 } 77 78 if _, _, err := w.doAddFile(idx, s, path, nil); err != nil { 79 return err 80 } 81 82 } 83 84 return w.r.Storer.SetIndex(idx) 85} 86 87func (w *Worktree) updateHEAD(commit plumbing.Hash) error { 88 head, err := w.r.Storer.Reference(plumbing.HEAD) 89 if err != nil { 90 return err 91 } 92 93 name := plumbing.HEAD 94 if head.Type() != plumbing.HashReference { 95 name = head.Target() 96 } 97 98 ref := plumbing.NewHashReference(name, commit) 99 return w.r.Storer.SetReference(ref) 100} 101 102func (w *Worktree) buildCommitObject(msg string, opts *CommitOptions, tree plumbing.Hash) (plumbing.Hash, error) { 103 commit := &object.Commit{ 104 Author: *opts.Author, 105 Committer: *opts.Committer, 106 Message: msg, 107 TreeHash: tree, 108 ParentHashes: opts.Parents, 109 } 110 111 if opts.SignKey != nil { 112 sig, err := w.buildCommitSignature(commit, opts.SignKey) 113 if err != nil { 114 return plumbing.ZeroHash, err 115 } 116 commit.PGPSignature = sig 117 } 118 119 obj := w.r.Storer.NewEncodedObject() 120 if err := commit.Encode(obj); err != nil { 121 return plumbing.ZeroHash, err 122 } 123 return w.r.Storer.SetEncodedObject(obj) 124} 125 126func (w *Worktree) buildCommitSignature(commit *object.Commit, signKey *openpgp.Entity) (string, error) { 127 encoded := &plumbing.MemoryObject{} 128 if err := commit.Encode(encoded); err != nil { 129 return "", err 130 } 131 r, err := encoded.Reader() 132 if err != nil { 133 return "", err 134 } 135 var b bytes.Buffer 136 if err := openpgp.ArmoredDetachSign(&b, signKey, r, nil); err != nil { 137 return "", err 138 } 139 return b.String(), nil 140} 141 142// buildTreeHelper converts a given index.Index file into multiple git objects 143// reading the blobs from the given filesystem and creating the trees from the 144// index structure. The created objects are pushed to a given Storer. 145type buildTreeHelper struct { 146 fs billy.Filesystem 147 s storage.Storer 148 149 trees map[string]*object.Tree 150 entries map[string]*object.TreeEntry 151} 152 153// BuildTree builds the tree objects and push its to the storer, the hash 154// of the root tree is returned. 155func (h *buildTreeHelper) BuildTree(idx *index.Index, opts *CommitOptions) (plumbing.Hash, error) { 156 if len(idx.Entries) == 0 && (opts == nil || !opts.AllowEmptyCommits) { 157 return plumbing.ZeroHash, ErrEmptyCommit 158 } 159 160 const rootNode = "" 161 h.trees = map[string]*object.Tree{rootNode: {}} 162 h.entries = map[string]*object.TreeEntry{} 163 164 for _, e := range idx.Entries { 165 if err := h.commitIndexEntry(e); err != nil { 166 return plumbing.ZeroHash, err 167 } 168 } 169 170 return h.copyTreeToStorageRecursive(rootNode, h.trees[rootNode]) 171} 172 173func (h *buildTreeHelper) commitIndexEntry(e *index.Entry) error { 174 parts := strings.Split(e.Name, "/") 175 176 var fullpath string 177 for _, part := range parts { 178 parent := fullpath 179 fullpath = path.Join(fullpath, part) 180 181 h.doBuildTree(e, parent, fullpath) 182 } 183 184 return nil 185} 186 187func (h *buildTreeHelper) doBuildTree(e *index.Entry, parent, fullpath string) { 188 if _, ok := h.trees[fullpath]; ok { 189 return 190 } 191 192 if _, ok := h.entries[fullpath]; ok { 193 return 194 } 195 196 te := object.TreeEntry{Name: path.Base(fullpath)} 197 198 if fullpath == e.Name { 199 te.Mode = e.Mode 200 te.Hash = e.Hash 201 } else { 202 te.Mode = filemode.Dir 203 h.trees[fullpath] = &object.Tree{} 204 } 205 206 h.trees[parent].Entries = append(h.trees[parent].Entries, te) 207} 208 209type sortableEntries []object.TreeEntry 210 211func (sortableEntries) sortName(te object.TreeEntry) string { 212 if te.Mode == filemode.Dir { 213 return te.Name + "/" 214 } 215 return te.Name 216} 217func (se sortableEntries) Len() int { return len(se) } 218func (se sortableEntries) Less(i int, j int) bool { return se.sortName(se[i]) < se.sortName(se[j]) } 219func (se sortableEntries) Swap(i int, j int) { se[i], se[j] = se[j], se[i] } 220 221func (h *buildTreeHelper) copyTreeToStorageRecursive(parent string, t *object.Tree) (plumbing.Hash, error) { 222 sort.Sort(sortableEntries(t.Entries)) 223 for i, e := range t.Entries { 224 if e.Mode != filemode.Dir && !e.Hash.IsZero() { 225 continue 226 } 227 228 path := path.Join(parent, e.Name) 229 230 var err error 231 e.Hash, err = h.copyTreeToStorageRecursive(path, h.trees[path]) 232 if err != nil { 233 return plumbing.ZeroHash, err 234 } 235 236 t.Entries[i] = e 237 } 238 239 o := h.s.NewEncodedObject() 240 if err := t.Encode(o); err != nil { 241 return plumbing.ZeroHash, err 242 } 243 244 hash := o.Hash() 245 if h.s.HasEncodedObject(hash) == nil { 246 return hash, nil 247 } 248 return h.s.SetEncodedObject(o) 249}