fork of go-git with some jj specific features
at v0.1.0 7.2 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 = headCommit.ParentHashes 57 } 58 59 idx, err := w.r.Storer.Index() 60 if err != nil { 61 return plumbing.ZeroHash, err 62 } 63 64 // First handle the case of the first commit in the repository being empty. 65 if len(opts.Parents) == 0 && len(idx.Entries) == 0 && !opts.AllowEmptyCommits { 66 return plumbing.ZeroHash, ErrEmptyCommit 67 } 68 69 h := &buildTreeHelper{ 70 fs: w.Filesystem, 71 s: w.r.Storer, 72 } 73 74 treeHash, err := h.BuildTree(idx, opts) 75 if err != nil { 76 return plumbing.ZeroHash, err 77 } 78 79 previousTree := plumbing.ZeroHash 80 if len(opts.Parents) > 0 { 81 parentCommit, err := w.r.CommitObject(opts.Parents[0]) 82 if err != nil { 83 return plumbing.ZeroHash, err 84 } 85 previousTree = parentCommit.TreeHash 86 } 87 88 if treeHash == previousTree && !opts.AllowEmptyCommits { 89 return plumbing.ZeroHash, ErrEmptyCommit 90 } 91 92 commit, err := w.buildCommitObject(msg, opts, treeHash) 93 if err != nil { 94 return plumbing.ZeroHash, err 95 } 96 97 return commit, w.updateHEAD(commit) 98} 99 100func (w *Worktree) autoAddModifiedAndDeleted() error { 101 s, err := w.Status() 102 if err != nil { 103 return err 104 } 105 106 idx, err := w.r.Storer.Index() 107 if err != nil { 108 return err 109 } 110 111 for path, fs := range s { 112 if fs.Worktree != Modified && fs.Worktree != Deleted { 113 continue 114 } 115 116 if _, _, err := w.doAddFile(idx, s, path, nil); err != nil { 117 return err 118 } 119 120 } 121 122 return w.r.Storer.SetIndex(idx) 123} 124 125func (w *Worktree) updateHEAD(commit plumbing.Hash) error { 126 head, err := w.r.Storer.Reference(plumbing.HEAD) 127 if err != nil { 128 return err 129 } 130 131 name := plumbing.HEAD 132 if head.Type() != plumbing.HashReference { 133 name = head.Target() 134 } 135 136 ref := plumbing.NewHashReference(name, commit) 137 return w.r.Storer.SetReference(ref) 138} 139 140func (w *Worktree) buildCommitObject(msg string, opts *CommitOptions, tree plumbing.Hash) (plumbing.Hash, error) { 141 commit := &object.Commit{ 142 Author: w.sanitize(*opts.Author), 143 Committer: w.sanitize(*opts.Committer), 144 Message: msg, 145 TreeHash: tree, 146 ParentHashes: opts.Parents, 147 } 148 149 // Convert SignKey into a Signer if set. Existing Signer should take priority. 150 signer := opts.Signer 151 if signer == nil && opts.SignKey != nil { 152 signer = &gpgSigner{key: opts.SignKey} 153 } 154 if signer != nil { 155 sig, err := signObject(signer, commit) 156 if err != nil { 157 return plumbing.ZeroHash, err 158 } 159 commit.PGPSignature = string(sig) 160 } 161 162 obj := w.r.Storer.NewEncodedObject() 163 if err := commit.Encode(obj); err != nil { 164 return plumbing.ZeroHash, err 165 } 166 return w.r.Storer.SetEncodedObject(obj) 167} 168 169func (w *Worktree) sanitize(signature object.Signature) object.Signature { 170 return object.Signature{ 171 Name: invalidCharactersRe.ReplaceAllString(signature.Name, ""), 172 Email: invalidCharactersRe.ReplaceAllString(signature.Email, ""), 173 When: signature.When, 174 } 175} 176 177type gpgSigner struct { 178 key *openpgp.Entity 179 cfg *packet.Config 180} 181 182func (s *gpgSigner) Sign(message io.Reader) ([]byte, error) { 183 var b bytes.Buffer 184 if err := openpgp.ArmoredDetachSign(&b, s.key, message, s.cfg); err != nil { 185 return nil, err 186 } 187 return b.Bytes(), nil 188} 189 190// buildTreeHelper converts a given index.Index file into multiple git objects 191// reading the blobs from the given filesystem and creating the trees from the 192// index structure. The created objects are pushed to a given Storer. 193type buildTreeHelper struct { 194 fs billy.Filesystem 195 s storage.Storer 196 197 trees map[string]*object.Tree 198 entries map[string]*object.TreeEntry 199} 200 201// BuildTree builds the tree objects and push its to the storer, the hash 202// of the root tree is returned. 203func (h *buildTreeHelper) BuildTree(idx *index.Index, opts *CommitOptions) (plumbing.Hash, error) { 204 const rootNode = "" 205 h.trees = map[string]*object.Tree{rootNode: {}} 206 h.entries = map[string]*object.TreeEntry{} 207 208 for _, e := range idx.Entries { 209 if err := h.commitIndexEntry(e); err != nil { 210 return plumbing.ZeroHash, err 211 } 212 } 213 214 return h.copyTreeToStorageRecursive(rootNode, h.trees[rootNode]) 215} 216 217func (h *buildTreeHelper) commitIndexEntry(e *index.Entry) error { 218 parts := strings.Split(e.Name, "/") 219 220 var fullpath string 221 for _, part := range parts { 222 parent := fullpath 223 fullpath = path.Join(fullpath, part) 224 225 h.doBuildTree(e, parent, fullpath) 226 } 227 228 return nil 229} 230 231func (h *buildTreeHelper) doBuildTree(e *index.Entry, parent, fullpath string) { 232 if _, ok := h.trees[fullpath]; ok { 233 return 234 } 235 236 if _, ok := h.entries[fullpath]; ok { 237 return 238 } 239 240 te := object.TreeEntry{Name: path.Base(fullpath)} 241 242 if fullpath == e.Name { 243 te.Mode = e.Mode 244 te.Hash = e.Hash 245 } else { 246 te.Mode = filemode.Dir 247 h.trees[fullpath] = &object.Tree{} 248 } 249 250 h.trees[parent].Entries = append(h.trees[parent].Entries, te) 251} 252 253type sortableEntries []object.TreeEntry 254 255func (sortableEntries) sortName(te object.TreeEntry) string { 256 if te.Mode == filemode.Dir { 257 return te.Name + "/" 258 } 259 return te.Name 260} 261func (se sortableEntries) Len() int { return len(se) } 262func (se sortableEntries) Less(i int, j int) bool { return se.sortName(se[i]) < se.sortName(se[j]) } 263func (se sortableEntries) Swap(i int, j int) { se[i], se[j] = se[j], se[i] } 264 265func (h *buildTreeHelper) copyTreeToStorageRecursive(parent string, t *object.Tree) (plumbing.Hash, error) { 266 sort.Sort(sortableEntries(t.Entries)) 267 for i, e := range t.Entries { 268 if e.Mode != filemode.Dir && !e.Hash.IsZero() { 269 continue 270 } 271 272 path := path.Join(parent, e.Name) 273 274 var err error 275 e.Hash, err = h.copyTreeToStorageRecursive(path, h.trees[path]) 276 if err != nil { 277 return plumbing.ZeroHash, err 278 } 279 280 t.Entries[i] = e 281 } 282 283 o := h.s.NewEncodedObject() 284 if err := t.Encode(o); err != nil { 285 return plumbing.ZeroHash, err 286 } 287 288 hash := o.Hash() 289 if h.s.HasEncodedObject(hash) == nil { 290 return hash, nil 291 } 292 return h.s.SetEncodedObject(o) 293}