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