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}