• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gittuf / gittuf / 27768702340

18 Jun 2026 03:01PM UTC coverage: 75.042%. Remained the same
27768702340

push

github

web-flow
Merge pull request #1400 from gittuf/bump-go-git

Bump go-git to v6

4 of 6 new or added lines in 2 files covered. (66.67%)

9351 of 12461 relevant lines covered (75.04%)

43.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.71
/pkg/gitinterface/commit.go
1
// Copyright The gittuf Authors
2
// SPDX-License-Identifier: Apache-2.0
3

4
package gitinterface
5

6
import (
7
        "context"
8
        "errors"
9
        "fmt"
10
        "io"
11
        "strings"
12
        "testing"
13
        "time"
14

15
        "github.com/gittuf/gittuf/internal/signerverifier/gpg"
16
        "github.com/gittuf/gittuf/internal/signerverifier/sigstore"
17
        "github.com/gittuf/gittuf/internal/signerverifier/ssh"
18
        "github.com/go-git/go-git/v6/plumbing"
19
        "github.com/go-git/go-git/v6/plumbing/object"
20
        "github.com/go-git/go-git/v6/storage/memory"
21
        "github.com/secure-systems-lab/go-securesystemslib/signerverifier"
22
)
23

24
// Commit creates a new commit in the repo and sets targetRef's to the commit.
25
// This function is meant only for gittuf references, and therefore it does not
26
// mutate repository worktrees.
27
func (r *Repository) Commit(treeID Hash, targetRef, message string, sign bool) (Hash, error) {
176✔
28
        currentGitID, err := r.GetReference(targetRef)
176✔
29
        if err != nil {
279✔
30
                if !errors.Is(err, ErrReferenceNotFound) {
103✔
31
                        return ZeroHash, err
×
32
                }
×
33
        }
34

35
        args := []string{"commit-tree", "-m", message}
176✔
36

176✔
37
        if !currentGitID.IsZero() {
249✔
38
                args = append(args, "-p", currentGitID.String())
73✔
39
        }
73✔
40

41
        if sign {
180✔
42
                args = append(args, "-S")
4✔
43
        }
4✔
44

45
        args = append(args, treeID.String())
176✔
46

176✔
47
        now := r.clock.Now().Format(time.RFC3339)
176✔
48
        env := []string{fmt.Sprintf("%s=%s", committerTimeKey, now), fmt.Sprintf("%s=%s", authorTimeKey, now)}
176✔
49

176✔
50
        stdOut, err := r.executor(args...).withEnv(env...).executeString()
176✔
51
        if err != nil {
176✔
52
                return ZeroHash, fmt.Errorf("unable to create commit: %w", err)
×
53
        }
×
54
        commitID, err := NewHash(stdOut)
176✔
55
        if err != nil {
176✔
56
                return ZeroHash, fmt.Errorf("received invalid commit ID: %w", err)
×
57
        }
×
58

59
        return commitID, r.CheckAndSetReference(targetRef, commitID, currentGitID)
176✔
60
}
61

62
// CommitUsingSpecificKey creates a new commit in the repository for the
63
// specified parameters. The commit is signed using the PEM encoded SSH or GPG
64
// private key. This function is expected for use in tests and gittuf's
65
// developer mode. In standard workflows, Commit() must be used instead which
66
// infers the signing key from the user's Git config.
67
func (r *Repository) CommitUsingSpecificKey(treeID Hash, targetRef, message string, signingKeyPEMBytes []byte) (Hash, error) {
4✔
68
        gitConfig, err := r.GetGitConfig()
4✔
69
        if err != nil {
4✔
70
                return ZeroHash, err
×
71
        }
×
72

73
        commitMetadata := object.Signature{
4✔
74
                Name:  gitConfig["user.name"],
4✔
75
                Email: gitConfig["user.email"],
4✔
76
                When:  r.clock.Now(),
4✔
77
        }
4✔
78

4✔
79
        commit := &object.Commit{
4✔
80
                Author:    commitMetadata,
4✔
81
                Committer: commitMetadata,
4✔
82
                TreeHash:  plumbing.NewHash(treeID.String()),
4✔
83
                Message:   message,
4✔
84
        }
4✔
85

4✔
86
        refTip, err := r.GetReference(targetRef)
4✔
87
        if err != nil {
6✔
88
                if !errors.Is(err, ErrReferenceNotFound) {
2✔
89
                        return ZeroHash, err
×
90
                }
×
91
        }
92

93
        if !refTip.IsZero() {
6✔
94
                commit.ParentHashes = []plumbing.Hash{plumbing.NewHash(refTip.String())}
2✔
95
        }
2✔
96

97
        commitContents, err := getCommitBytesWithoutSignature(commit)
4✔
98
        if err != nil {
4✔
99
                return ZeroHash, err
×
100
        }
×
101
        signature, err := signGitObjectUsingKey(commitContents, signingKeyPEMBytes)
4✔
102
        if err != nil {
5✔
103
                return ZeroHash, err
1✔
104
        }
1✔
105
        commit.Signature = signature
3✔
106

3✔
107
        goGitRepo, err := r.GetGoGitRepository()
3✔
108
        if err != nil {
3✔
109
                return ZeroHash, err
×
110
        }
×
111

112
        obj := goGitRepo.Storer.NewEncodedObject()
3✔
113
        if err := commit.Encode(obj); err != nil {
3✔
114
                return ZeroHash, err
×
115
        }
×
116
        commitID, err := goGitRepo.Storer.SetEncodedObject(obj)
3✔
117
        if err != nil {
3✔
118
                return ZeroHash, err
×
119
        }
×
120

121
        commitIDHash, err := NewHash(commitID.String())
3✔
122
        if err != nil {
3✔
123
                return ZeroHash, err
×
124
        }
×
125

126
        return commitIDHash, r.CheckAndSetReference(targetRef, commitIDHash, refTip)
3✔
127
}
128

129
// commitWithParents creates a new commit in the repo but does not update any
130
// references. It is only meant to be used for tests, and therefore accepts
131
// specific parent commit IDs.
132
func (r *Repository) commitWithParents(t *testing.T, treeID Hash, parentIDs []Hash, message string, sign bool) Hash { //nolint:unparam
14✔
133
        args := []string{"commit-tree", "-m", message}
14✔
134

14✔
135
        for _, commitID := range parentIDs {
33✔
136
                args = append(args, "-p", commitID.String())
19✔
137
        }
19✔
138

139
        if sign {
14✔
140
                args = append(args, "-S")
×
141
        }
×
142

143
        args = append(args, treeID.String())
14✔
144

14✔
145
        now := r.clock.Now().Format(time.RFC3339)
14✔
146
        env := []string{fmt.Sprintf("%s=%s", committerTimeKey, now), fmt.Sprintf("%s=%s", authorTimeKey, now)}
14✔
147

14✔
148
        stdOut, err := r.executor(args...).withEnv(env...).executeString()
14✔
149
        if err != nil {
14✔
150
                t.Fatal(fmt.Errorf("unable to create commit: %w", err))
×
151
        }
×
152
        commitID, err := NewHash(stdOut)
14✔
153
        if err != nil {
14✔
154
                t.Fatal(fmt.Errorf("received invalid commit ID: %w", err))
×
155
        }
×
156

157
        return commitID
14✔
158
}
159

160
// verifyCommitSignature verifies a signature for the specified commit using
161
// the provided public key.
162
func (r *Repository) verifyCommitSignature(ctx context.Context, commitID Hash, key *signerverifier.SSLibKey) error {
7✔
163
        goGitRepo, err := r.GetGoGitRepository()
7✔
164
        if err != nil {
7✔
165
                return fmt.Errorf("error opening repository: %w", err)
×
166
        }
×
167

168
        commit, err := goGitRepo.CommitObject(plumbing.NewHash(commitID.String()))
7✔
169
        if err != nil {
7✔
170
                return fmt.Errorf("unable to load commit object: %w", err)
×
171
        }
×
172

173
        switch key.KeyType {
7✔
174
        case gpg.KeyType:
2✔
175
                if _, err := commit.Verify(key.KeyVal.Public); err != nil {
3✔
176
                        return ErrIncorrectVerificationKey
1✔
177
                }
1✔
178

179
                return nil
1✔
180
        case ssh.KeyType:
4✔
181
                commitContents, err := getCommitBytesWithoutSignature(commit)
4✔
182
                if err != nil {
4✔
183
                        return errors.Join(ErrVerifyingSSHSignature, err)
×
184
                }
×
185
                commitSignature := []byte(commit.Signature)
4✔
186

4✔
187
                if err := verifySSHKeySignature(ctx, key, commitContents, commitSignature); err != nil {
6✔
188
                        return errors.Join(ErrIncorrectVerificationKey, err)
2✔
189
                }
2✔
190

191
                return nil
2✔
192
        case sigstore.KeyType:
×
193
                commitContents, err := getCommitBytesWithoutSignature(commit)
×
194
                if err != nil {
×
195
                        return errors.Join(ErrVerifyingSigstoreSignature, err)
×
196
                }
×
NEW
197
                commitSignature := []byte(commit.Signature)
×
198

×
199
                if err := verifyGitsignSignature(ctx, r, key, commitContents, commitSignature); err != nil {
×
200
                        return errors.Join(ErrIncorrectVerificationKey, err)
×
201
                }
×
202

203
                return nil
×
204
        }
205

206
        return ErrUnknownSigningMethod
1✔
207
}
208

209
// GetCommitMessage returns the commit's message.
210
func (r *Repository) GetCommitMessage(commitID Hash) (string, error) {
2✔
211
        if err := r.ensureIsCommit(commitID); err != nil {
3✔
212
                return "", err
1✔
213
        }
1✔
214

215
        commitMessage, err := r.executor("show", "-s", "--format=%B", commitID.String()).executeString()
1✔
216
        if err != nil {
1✔
217
                return "", fmt.Errorf("unable to identify message for commit '%s': %w", commitID.String(), err)
×
218
        }
×
219

220
        return commitMessage, nil
1✔
221
}
222

223
// GetCommitTreeID returns the commit's Git tree ID.
224
func (r *Repository) GetCommitTreeID(commitID Hash) (Hash, error) {
103✔
225
        if err := r.ensureIsCommit(commitID); err != nil {
104✔
226
                return ZeroHash, err
1✔
227
        }
1✔
228

229
        stdOut, err := r.executor("rev-parse", fmt.Sprintf("%s^{tree}", commitID.String())).executeString()
102✔
230
        if err != nil {
102✔
231
                return ZeroHash, fmt.Errorf("unable to identify tree for commit '%s': %w", commitID.String(), err)
×
232
        }
×
233

234
        hash, err := NewHash(stdOut)
102✔
235
        if err != nil {
102✔
236
                return ZeroHash, fmt.Errorf("invalid tree for commit ID '%s': %w", commitID, err)
×
237
        }
×
238
        return hash, nil
102✔
239
}
240

241
// GetCommitParentIDs returns the commit's parent commit IDs.
242
func (r *Repository) GetCommitParentIDs(commitID Hash) ([]Hash, error) {
14✔
243
        if err := r.ensureIsCommit(commitID); err != nil {
15✔
244
                return nil, err
1✔
245
        }
1✔
246

247
        stdOut, err := r.executor("rev-parse", fmt.Sprintf("%s^@", commitID.String())).executeString()
13✔
248
        if err != nil {
13✔
249
                return nil, fmt.Errorf("unable to identify parents for commit '%s': %w", commitID.String(), err)
×
250
        }
×
251

252
        commitIDSplit := strings.Split(stdOut, "\n")
13✔
253
        if len(commitIDSplit) == 0 {
13✔
254
                return nil, nil
×
255
        }
×
256

257
        commitIDs := []Hash{}
13✔
258
        for _, commitID := range commitIDSplit {
30✔
259
                if commitID == "" {
19✔
260
                        continue
2✔
261
                }
262

263
                hash, err := NewHash(commitID)
15✔
264
                if err != nil {
15✔
265
                        return nil, fmt.Errorf("invalid parent commit ID '%s': %w", commitID, err)
×
266
                }
×
267

268
                commitIDs = append(commitIDs, hash)
15✔
269
        }
270

271
        if len(commitIDs) == 0 {
15✔
272
                return nil, nil
2✔
273
        }
2✔
274

275
        return commitIDs, nil
11✔
276
}
277

278
// KnowsCommit returns true if the `testCommit` is a descendent of the
279
// `ancestorCommit`. That is, the testCommit _knows_ the ancestorCommit as it
280
// has a path in the commit graph to the ancestorCommit.
281
func (r *Repository) KnowsCommit(testCommitID, ancestorCommitID Hash) (bool, error) {
7✔
282
        if err := r.ensureIsCommit(testCommitID); err != nil {
8✔
283
                return false, err
1✔
284
        }
1✔
285
        if err := r.ensureIsCommit(ancestorCommitID); err != nil {
7✔
286
                return false, err
1✔
287
        }
1✔
288

289
        _, err := r.executor("merge-base", "--is-ancestor", ancestorCommitID.String(), testCommitID.String()).executeString()
5✔
290
        return err == nil, nil
5✔
291
}
292

293
// GetCommonAncestor finds the common ancestor commit for the two supplied
294
// commits.
295
func (r *Repository) GetCommonAncestor(commitAID, commitBID Hash) (Hash, error) {
4✔
296
        if err := r.ensureIsCommit(commitAID); err != nil {
5✔
297
                return nil, err
1✔
298
        }
1✔
299
        if err := r.ensureIsCommit(commitBID); err != nil {
4✔
300
                return nil, err
1✔
301
        }
1✔
302

303
        mergeBase, err := r.executor("merge-base", commitAID.String(), commitBID.String()).executeString()
2✔
304
        if err != nil {
3✔
305
                return nil, err
1✔
306
        }
1✔
307

308
        mergeBaseID, err := NewHash(mergeBase)
1✔
309
        if err != nil {
1✔
310
                return nil, fmt.Errorf("received invalid commit ID: %w", err)
×
311
        }
×
312
        return mergeBaseID, nil
1✔
313
}
314

315
// ensureIsCommit is a helper to check that the ID represents a Git commit
316
// object.
317
func (r *Repository) ensureIsCommit(commitID Hash) error {
162✔
318
        objType, err := r.executor("cat-file", "-t", commitID.String()).executeString()
162✔
319
        if err != nil {
163✔
320
                return fmt.Errorf("unable to inspect if object is commit: %w", err)
1✔
321
        } else if objType != "commit" {
174✔
322
                return fmt.Errorf("requested Git ID '%s' is not a commit object", commitID.String())
12✔
323
        }
12✔
324

325
        return nil
149✔
326
}
327

328
func getCommitBytesWithoutSignature(commit *object.Commit) ([]byte, error) {
8✔
329
        commitEncoded := memory.NewStorage().NewEncodedObject()
8✔
330
        if err := commit.EncodeWithoutSignature(commitEncoded); err != nil {
8✔
331
                return nil, err
×
332
        }
×
333
        r, err := commitEncoded.Reader()
8✔
334
        if err != nil {
8✔
335
                return nil, err
×
336
        }
×
337

338
        return io.ReadAll(r)
8✔
339
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc