1package git
2
3import (
4 "bytes"
5 "errors"
6 "io"
7 "path"
8 "regexp"
9 "sort"
10 "strings"
11
12 "github.com/go-git/go-git/v5/plumbing"
13 "github.com/go-git/go-git/v5/plumbing/filemode"
14 "github.com/go-git/go-git/v5/plumbing/format/index"
15 "github.com/go-git/go-git/v5/plumbing/object"
16 "github.com/go-git/go-git/v5/storage"
17
18 "github.com/ProtonMail/go-crypto/openpgp"
19 "github.com/ProtonMail/go-crypto/openpgp/packet"
20 "github.com/go-git/go-billy/v5"
21)
22
23var (
24 // ErrEmptyCommit occurs when a commit is attempted using a clean
25 // working tree, with no changes to be committed.
26 ErrEmptyCommit = errors.New("cannot create empty commit: clean working tree")
27
28 // characters to be removed from user name and/or email before using them to build a commit object
29 // See https://git-scm.com/docs/git-commit#_commit_information
30 invalidCharactersRe = regexp.MustCompile(`[<>\n]`)
31)
32
33// Commit stores the current contents of the index in a new commit along with
34// a log message from the user describing the changes.
35func (w *Worktree) Commit(msg string, opts *CommitOptions) (plumbing.Hash, error) {
36 if err := opts.Validate(w.r); err != nil {
37 return plumbing.ZeroHash, err
38 }
39
40 if opts.All {
41 if err := w.autoAddModifiedAndDeleted(); err != nil {
42 return plumbing.ZeroHash, err
43 }
44 }
45
46 if opts.Amend {
47 head, err := w.r.Head()
48 if err != nil {
49 return plumbing.ZeroHash, err
50 }
51 headCommit, err := w.r.CommitObject(head.Hash())
52 if err != nil {
53 return plumbing.ZeroHash, err
54 }
55
56 opts.Parents = nil
57 if len(headCommit.ParentHashes) != 0 {
58 opts.Parents = []plumbing.Hash{headCommit.ParentHashes[0]}
59 }
60 }
61
62 idx, err := w.r.Storer.Index()
63 if err != nil {
64 return plumbing.ZeroHash, err
65 }
66
67 // First handle the case of the first commit in the repository being empty.
68 if len(opts.Parents) == 0 && len(idx.Entries) == 0 && !opts.AllowEmptyCommits {
69 return plumbing.ZeroHash, ErrEmptyCommit
70 }
71
72 h := &buildTreeHelper{
73 fs: w.Filesystem,
74 s: w.r.Storer,
75 }
76
77 treeHash, err := h.BuildTree(idx, opts)
78 if err != nil {
79 return plumbing.ZeroHash, err
80 }
81
82 previousTree := plumbing.ZeroHash
83 if len(opts.Parents) > 0 {
84 parentCommit, err := w.r.CommitObject(opts.Parents[0])
85 if err != nil {
86 return plumbing.ZeroHash, err
87 }
88 previousTree = parentCommit.TreeHash
89 }
90
91 if treeHash == previousTree && !opts.AllowEmptyCommits {
92 return plumbing.ZeroHash, ErrEmptyCommit
93 }
94
95 commit, err := w.buildCommitObject(msg, opts, treeHash)
96 if err != nil {
97 return plumbing.ZeroHash, err
98 }
99
100 return commit, w.updateHEAD(commit)
101}
102
103func (w *Worktree) autoAddModifiedAndDeleted() error {
104 s, err := w.Status()
105 if err != nil {
106 return err
107 }
108
109 idx, err := w.r.Storer.Index()
110 if err != nil {
111 return err
112 }
113
114 for path, fs := range s {
115 if fs.Worktree != Modified && fs.Worktree != Deleted {
116 continue
117 }
118
119 if _, _, err := w.doAddFile(idx, s, path, nil); err != nil {
120 return err
121 }
122
123 }
124
125 return w.r.Storer.SetIndex(idx)
126}
127
128func (w *Worktree) updateHEAD(commit plumbing.Hash) error {
129 head, err := w.r.Storer.Reference(plumbing.HEAD)
130 if err != nil {
131 return err
132 }
133
134 name := plumbing.HEAD
135 if head.Type() != plumbing.HashReference {
136 name = head.Target()
137 }
138
139 ref := plumbing.NewHashReference(name, commit)
140 return w.r.Storer.SetReference(ref)
141}
142
143func (w *Worktree) buildCommitObject(msg string, opts *CommitOptions, tree plumbing.Hash) (plumbing.Hash, error) {
144 commit := &object.Commit{
145 Author: w.sanitize(*opts.Author),
146 Committer: w.sanitize(*opts.Committer),
147 Message: msg,
148 TreeHash: tree,
149 ParentHashes: opts.Parents,
150 }
151
152 // Convert SignKey into a Signer if set. Existing Signer should take priority.
153 signer := opts.Signer
154 if signer == nil && opts.SignKey != nil {
155 signer = &gpgSigner{key: opts.SignKey}
156 }
157 if signer != nil {
158 sig, err := signObject(signer, commit)
159 if err != nil {
160 return plumbing.ZeroHash, err
161 }
162 commit.PGPSignature = string(sig)
163 }
164
165 obj := w.r.Storer.NewEncodedObject()
166 if err := commit.Encode(obj); err != nil {
167 return plumbing.ZeroHash, err
168 }
169 return w.r.Storer.SetEncodedObject(obj)
170}
171
172func (w *Worktree) sanitize(signature object.Signature) object.Signature {
173 return object.Signature{
174 Name: invalidCharactersRe.ReplaceAllString(signature.Name, ""),
175 Email: invalidCharactersRe.ReplaceAllString(signature.Email, ""),
176 When: signature.When,
177 }
178}
179
180type gpgSigner struct {
181 key *openpgp.Entity
182 cfg *packet.Config
183}
184
185func (s *gpgSigner) Sign(message io.Reader) ([]byte, error) {
186 var b bytes.Buffer
187 if err := openpgp.ArmoredDetachSign(&b, s.key, message, s.cfg); err != nil {
188 return nil, err
189 }
190 return b.Bytes(), nil
191}
192
193// buildTreeHelper converts a given index.Index file into multiple git objects
194// reading the blobs from the given filesystem and creating the trees from the
195// index structure. The created objects are pushed to a given Storer.
196type buildTreeHelper struct {
197 fs billy.Filesystem
198 s storage.Storer
199
200 trees map[string]*object.Tree
201 entries map[string]*object.TreeEntry
202}
203
204// BuildTree builds the tree objects and push its to the storer, the hash
205// of the root tree is returned.
206func (h *buildTreeHelper) BuildTree(idx *index.Index, opts *CommitOptions) (plumbing.Hash, error) {
207 const rootNode = ""
208 h.trees = map[string]*object.Tree{rootNode: {}}
209 h.entries = map[string]*object.TreeEntry{}
210
211 for _, e := range idx.Entries {
212 if err := h.commitIndexEntry(e); err != nil {
213 return plumbing.ZeroHash, err
214 }
215 }
216
217 return h.copyTreeToStorageRecursive(rootNode, h.trees[rootNode])
218}
219
220func (h *buildTreeHelper) commitIndexEntry(e *index.Entry) error {
221 parts := strings.Split(e.Name, "/")
222
223 var fullpath string
224 for _, part := range parts {
225 parent := fullpath
226 fullpath = path.Join(fullpath, part)
227
228 h.doBuildTree(e, parent, fullpath)
229 }
230
231 return nil
232}
233
234func (h *buildTreeHelper) doBuildTree(e *index.Entry, parent, fullpath string) {
235 if _, ok := h.trees[fullpath]; ok {
236 return
237 }
238
239 if _, ok := h.entries[fullpath]; ok {
240 return
241 }
242
243 te := object.TreeEntry{Name: path.Base(fullpath)}
244
245 if fullpath == e.Name {
246 te.Mode = e.Mode
247 te.Hash = e.Hash
248 } else {
249 te.Mode = filemode.Dir
250 h.trees[fullpath] = &object.Tree{}
251 }
252
253 h.trees[parent].Entries = append(h.trees[parent].Entries, te)
254}
255
256type sortableEntries []object.TreeEntry
257
258func (sortableEntries) sortName(te object.TreeEntry) string {
259 if te.Mode == filemode.Dir {
260 return te.Name + "/"
261 }
262 return te.Name
263}
264func (se sortableEntries) Len() int { return len(se) }
265func (se sortableEntries) Less(i int, j int) bool { return se.sortName(se[i]) < se.sortName(se[j]) }
266func (se sortableEntries) Swap(i int, j int) { se[i], se[j] = se[j], se[i] }
267
268func (h *buildTreeHelper) copyTreeToStorageRecursive(parent string, t *object.Tree) (plumbing.Hash, error) {
269 sort.Sort(sortableEntries(t.Entries))
270 for i, e := range t.Entries {
271 if e.Mode != filemode.Dir && !e.Hash.IsZero() {
272 continue
273 }
274
275 path := path.Join(parent, e.Name)
276
277 var err error
278 e.Hash, err = h.copyTreeToStorageRecursive(path, h.trees[path])
279 if err != nil {
280 return plumbing.ZeroHash, err
281 }
282
283 t.Entries[i] = e
284 }
285
286 o := h.s.NewEncodedObject()
287 if err := t.Encode(o); err != nil {
288 return plumbing.ZeroHash, err
289 }
290
291 hash := o.Hash()
292 if h.s.HasEncodedObject(hash) == nil {
293 return hash, nil
294 }
295 return h.s.SetEncodedObject(o)
296}