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}