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