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