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