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