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