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

gittuf / gittuf / 13148437301

05 Feb 2025 01:40AM UTC coverage: 63.172% (+0.05%) from 63.124%
13148437301

push

github

web-flow
Merge pull request #782 from gittuf/gitinterface-propagate

gitinterface: Add subtree workflow

73 of 116 new or added lines in 3 files covered. (62.93%)

5779 of 9148 relevant lines covered (63.17%)

53.93 hits per line

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

72.56
/internal/gitinterface/tree.go
1
// Copyright The gittuf Authors
2
// SPDX-License-Identifier: Apache-2.0
3

4
package gitinterface
5

6
import (
7
        "bytes"
8
        "errors"
9
        "fmt"
10
        "os"
11
        "path"
12
        "strings"
13
)
14

15
var (
16
        ErrTreeDoesNotHavePath             = errors.New("tree does not have requested path")
17
        ErrCopyingBlobIDsDoNotMatch        = errors.New("blob ID in local repository does not match upstream repository")
18
        ErrCannotCreateSubtreeIntoRootTree = errors.New("subtree path target cannot be empty or root of tree")
19
)
20

21
func (r *Repository) EmptyTree() (Hash, error) {
1✔
22
        treeID, err := r.executor("hash-object", "-t", "tree", "--stdin").executeString()
1✔
23
        if err != nil {
1✔
24
                return ZeroHash, fmt.Errorf("unable to hash empty tree: %w", err)
×
25
        }
×
26

27
        hash, err := NewHash(treeID)
1✔
28
        if err != nil {
1✔
29
                return ZeroHash, fmt.Errorf("empty tree has invalid Git ID: %w", err)
×
30
        }
×
31

32
        return hash, nil
1✔
33
}
34

35
// GetPathIDInTree returns the Git ID pointed to by the path in the specified
36
// tree if the path exists. If not, a corresponding error is returned.  For
37
// example, if the tree contains a single blob `foo/bar/baz`, querying the ID
38
// for `foo/bar/baz` will return the blob ID for baz. Querying the ID for
39
// `foo/bar` will return the intermediate tree ID for bar, while querying for
40
// `foo/baz` will return an error.
41
func (r *Repository) GetPathIDInTree(treePath string, treeID Hash) (Hash, error) {
27✔
42
        treePath = strings.TrimSuffix(treePath, "/")
27✔
43
        components := strings.Split(treePath, "/")
27✔
44

27✔
45
        currentTreeID := treeID
27✔
46
        for len(components) != 0 {
71✔
47
                items, err := r.GetTreeItems(currentTreeID)
44✔
48
                if err != nil {
44✔
49
                        return nil, err
×
50
                }
×
51

52
                entryID, has := items[components[0]]
44✔
53
                if !has {
46✔
54
                        return nil, fmt.Errorf("%w: %s", ErrTreeDoesNotHavePath, treePath)
2✔
55
                }
2✔
56

57
                currentTreeID = entryID
42✔
58
                components = components[1:]
42✔
59
        }
60

61
        return currentTreeID, nil
25✔
62
}
63

64
// GetTreeItems returns the items in a specified Git tree without recursively
65
// expanding subtrees.
66
func (r *Repository) GetTreeItems(treeID Hash) (map[string]Hash, error) {
48✔
67
        // From Git 2.36, we can use --format here. However, it appears a not
48✔
68
        // insignificant number of developers are still on Git 2.34.1, a side effect
48✔
69
        // of being on Ubuntu 22.04. 22.04 is still widely used in WSL2 environments.
48✔
70
        // So, we're removing --format and parsing the output differently to handle
48✔
71
        // the extra information for each entry we don't need.
48✔
72
        stdOut, err := r.executor("ls-tree", treeID.String()).executeString()
48✔
73
        if err != nil {
48✔
74
                return nil, fmt.Errorf("unable to enumerate items in tree '%s': %w", treeID.String(), err)
×
75
        }
×
76

77
        if stdOut == "" {
50✔
78
                return nil, nil // alternatively, just check if treeID is empty tree?
2✔
79
        }
2✔
80

81
        entries := strings.Split(stdOut, "\n")
46✔
82
        if len(entries) == 0 {
46✔
83
                return nil, nil
×
84
        }
×
85

86
        items := map[string]Hash{}
46✔
87
        for _, entry := range entries {
125✔
88
                // Without --format, the output is in the following format:
79✔
89
                // <mode> SP <type> SP <object> TAB <file>
79✔
90
                // From: https://git-scm.com/docs/git-ls-tree/2.34.1#_output_format
79✔
91

79✔
92
                entrySplit := strings.Split(entry, " ")
79✔
93
                // entrySplit[0] is <mode> -- discard
79✔
94
                // entrySplit[1] is <type> -- discard
79✔
95
                // entrySplit[2] is <object> TAB <file> -- keep
79✔
96
                entrySplit = strings.Split(entrySplit[2], "\t")
79✔
97

79✔
98
                // <object> is really the object ID
79✔
99
                hash, err := NewHash(entrySplit[0])
79✔
100
                if err != nil {
79✔
101
                        return nil, fmt.Errorf("invalid Git ID '%s' for path '%s': %w", entrySplit[0], entrySplit[1], err)
×
102
                }
×
103

104
                items[entrySplit[1]] = hash
79✔
105
        }
106

107
        return items, nil
46✔
108
}
109

110
// GetAllFilesInTree returns all filepaths and the corresponding blob hashes in
111
// the specified tree.
112
func (r *Repository) GetAllFilesInTree(treeID Hash) (map[string]Hash, error) {
27✔
113
        // From Git 2.36, we can use --format here. However, it appears a not
27✔
114
        // insignificant number of developers are still on Git 2.34.1, a side effect
27✔
115
        // of being on Ubuntu 22.04. 22.04 is still widely used in WSL2 environments.
27✔
116
        // So, we're removing --format and parsing the output differently to handle
27✔
117
        // the extra information for each entry we don't need.
27✔
118
        stdOut, err := r.executor("ls-tree", "-r", treeID.String()).executeString()
27✔
119
        if err != nil {
31✔
120
                return nil, fmt.Errorf("unable to enumerate all files in tree: %w", err)
4✔
121
        }
4✔
122

123
        if stdOut == "" {
23✔
124
                return nil, nil // alternatively, just check if treeID is empty tree?
×
125
        }
×
126

127
        entries := strings.Split(stdOut, "\n")
23✔
128
        if len(entries) == 0 {
23✔
129
                return nil, nil
×
130
        }
×
131

132
        files := map[string]Hash{}
23✔
133
        for _, entry := range entries {
77✔
134
                // Without --format, the output is in the following format:
54✔
135
                // <mode> SP <type> SP <object> TAB <file>
54✔
136
                // From: https://git-scm.com/docs/git-ls-tree/2.34.1#_output_format
54✔
137

54✔
138
                entrySplit := strings.Split(entry, " ")
54✔
139
                // entrySplit[0] is <mode> -- discard
54✔
140
                // entrySplit[1] is <type> -- discard
54✔
141
                // entrySplit[2] is <object> TAB <file> -- keep
54✔
142
                entrySplit = strings.Split(entrySplit[2], "\t")
54✔
143

54✔
144
                // <object> is really the object ID
54✔
145
                hash, err := NewHash(entrySplit[0])
54✔
146
                if err != nil {
54✔
147
                        return nil, fmt.Errorf("invalid Git ID '%s' for path '%s': %w", entrySplit[0], entrySplit[1], err)
×
148
                }
×
149

150
                files[entrySplit[1]] = hash
54✔
151
        }
152

153
        return files, nil
23✔
154
}
155

156
// GetMergeTree computes the merge tree for the commits passed in. The tree is
157
// not written to the object store. Assuming a typical merge workflow, the first
158
// commit is expected to be the tip of the base branch. As such, the second
159
// commit is expected to be merged into the first. If the first commit is zero,
160
// the second commit's tree is returned.
161
func (r *Repository) GetMergeTree(commitAID, commitBID Hash) (Hash, error) {
3✔
162
        if err := r.ensureIsCommit(commitBID); err != nil {
3✔
163
                return ZeroHash, err
×
164
        }
×
165

166
        if commitAID.IsZero() {
4✔
167
                // fast-forward merge -> use tree ID from commitB
1✔
168
                return r.GetCommitTreeID(commitBID)
1✔
169
        }
1✔
170

171
        // Only commitB needs to be non-zero, we can allow fast-forward merges when
172
        // the base commit is zero. So, check this only after above
173
        if err := r.ensureIsCommit(commitAID); err != nil {
2✔
174
                return ZeroHash, err
×
175
        }
×
176

177
        niceGit, err := isNiceGitVersion()
2✔
178
        if err != nil {
2✔
179
                return ZeroHash, err
×
180
        }
×
181

182
        var stdOut string
2✔
183
        if !niceGit {
2✔
184
                // Older Git versions do not support merge-tree, and, as such, require
×
185
                // quite a long workaround to find what the merge tree is. This
×
186
                // workaround boils down to:
×
187
                // Create new branch > Merge into said branch > Extract tree hash
×
188
                currentBranch, err := r.executor("branch", "--show-current").executeString()
×
189
                if err != nil {
×
190
                        return ZeroHash, fmt.Errorf("unable to determine current branch: %w", err)
×
191
                }
×
192

193
                if currentBranch == "" {
×
194
                        return ZeroHash, fmt.Errorf("currently in detached HEAD state, please switch to a branch")
×
195
                }
×
196

197
                _, err = r.executor("checkout", commitAID.String()).executeString()
×
198
                if err != nil {
×
199
                        return ZeroHash, fmt.Errorf("unable to enter detached HEAD state: %w", err)
×
200
                }
×
201

202
                _, err = r.executor("merge", "-m", "Computing merge tree", commitBID.String()).executeString()
×
203
                if err != nil {
×
204
                        // Attempt to abort the merge in all cases as a failsafe
×
205
                        _, abrtErr := r.executor("merge", "--abort").executeString()
×
206
                        if abrtErr != nil {
×
207
                                return ZeroHash, fmt.Errorf("unable to perform merge, and unable to abort merge: %w, %w", err, abrtErr)
×
208
                        }
×
209

210
                        return ZeroHash, fmt.Errorf("unable to perform merge: %w", err)
×
211
                }
212

213
                stdOut, err = r.executor("show", "-s", "--format=%T").executeString()
×
214
                if err != nil {
×
215
                        return ZeroHash, fmt.Errorf("unable to extract tree hash of merge commit: %w", err)
×
216
                }
×
217

218
                // Switch back to the branch the user was on
219
                _, err = r.executor("checkout", currentBranch).executeString()
×
220
                if err != nil {
×
221
                        return ZeroHash, fmt.Errorf("unable to switch back to original branch: %w", err)
×
222
                }
×
223
        } else {
2✔
224
                stdOut, err = r.executor("merge-tree", commitAID.String(), commitBID.String()).executeString()
2✔
225
                if err != nil {
3✔
226
                        return ZeroHash, fmt.Errorf("unable to compute merge tree: %w", err)
1✔
227
                }
1✔
228
        }
229

230
        treeHash, err := NewHash(stdOut)
1✔
231
        if err != nil {
1✔
232
                return ZeroHash, fmt.Errorf("invalid merge tree ID: %w", err)
×
233
        }
×
234

235
        return treeHash, nil
1✔
236
}
237

238
// CreateSubtreeFromUpstreamRepository accepts an upstream repository handler
239
// and a commit ID in the upstream repository. This information is used to copy
240
// the entire contents of the commit's Git tree into the specified localPath in
241
// the localRef. A new commit is added to localRef with the changes made to
242
// localPath. localPath represents a directory path where the changes are copied
243
// to. Existing items in that directory are overwritten in the subsequently
244
// created commit in localRef. localPath must be specified, if left blank (say
245
// to imply copying into the root directory of the downstream repository),
246
// creating a subtree will fail.
247
func (r *Repository) CreateSubtreeFromUpstreamRepository(upstream *Repository, upstreamCommitID Hash, localRef, localPath string) (Hash, error) {
14✔
248
        if localPath == "" {
15✔
249
                return nil, ErrCannotCreateSubtreeIntoRootTree
1✔
250
        }
1✔
251
        currentTip, err := r.GetReference(localRef)
13✔
252
        if err != nil {
17✔
253
                if !errors.Is(err, ErrReferenceNotFound) {
4✔
NEW
254
                        return nil, err
×
NEW
255
                }
×
256
        }
257

258
        entries := []TreeEntry{}
13✔
259
        if !currentTip.IsZero() {
22✔
260
                currentRefTree, err := r.GetCommitTreeID(currentTip)
9✔
261
                if err != nil {
9✔
NEW
262
                        return nil, err
×
NEW
263
                }
×
264
                currentFiles, err := r.GetAllFilesInTree(currentRefTree)
9✔
265
                if err != nil {
9✔
NEW
266
                        return nil, err
×
NEW
267
                }
×
268

269
                // Ignore entries for `localPath` to account for upstream deletions
270
                // If localPath is foo/, we want to ignore all items under foo/
271
                // If localPath is foo, we want to ignore all items under foo/
272
                // If localPath is foo, we DO NOT want to remove all items under foobar/
273
                // So, add the / suffix if necessary to localPath
274
                if !strings.HasSuffix(localPath, "/") {
14✔
275
                        localPath += "/"
5✔
276
                }
5✔
277

278
                // Create list of TreeEntry objects representing all blobs except those
279
                // currently under localPath
280
                for filePath, blobID := range currentFiles {
31✔
281
                        if !strings.HasPrefix(filePath, localPath) {
40✔
282
                                entries = append(entries, NewEntryBlob(filePath, blobID))
18✔
283
                        }
18✔
284
                }
285
        }
286

287
        // Remove trailing "/" now
288
        localPath = strings.TrimSuffix(localPath, "/")
13✔
289

13✔
290
        treeID, err := upstream.GetCommitTreeID(upstreamCommitID)
13✔
291
        if err != nil {
13✔
NEW
292
                return nil, err
×
NEW
293
        }
×
294

295
        if r.HasObject(treeID) {
24✔
296
                // Use existing intermediate tree
11✔
297
                entries = append(entries, NewEntryTree(localPath, treeID))
11✔
298
        } else {
13✔
299
                // We have to create the intermediate tree for localPath
2✔
300
                filesToCopy, err := upstream.GetAllFilesInTree(treeID)
2✔
301
                if err != nil {
2✔
NEW
302
                        return nil, err
×
NEW
303
                }
×
304

305
                for blobPath, blobID := range filesToCopy {
10✔
306
                        // if blob already exists, we don't need to carry out expensive
8✔
307
                        // read/write
8✔
308
                        if !r.HasObject(blobID) {
8✔
NEW
309
                                blob, err := upstream.ReadBlob(blobID)
×
NEW
310
                                if err != nil {
×
NEW
311
                                        return nil, err
×
NEW
312
                                }
×
NEW
313
                                localBlobID, err := r.WriteBlob(blob)
×
NEW
314
                                if err != nil {
×
NEW
315
                                        return nil, err
×
NEW
316
                                }
×
NEW
317
                                if !localBlobID.Equal(blobID) {
×
NEW
318
                                        return nil, ErrCopyingBlobIDsDoNotMatch
×
NEW
319
                                }
×
320
                        }
321

322
                        // add blob to entries, with the path including the localPath prefix
323
                        entries = append(entries, NewEntryBlob(path.Join(localPath, blobPath), blobID))
8✔
324
                }
325
        }
326

327
        treeBuilder := NewTreeBuilder(r)
13✔
328
        newTreeID, err := treeBuilder.WriteTreeFromEntries(entries)
13✔
329
        if err != nil {
13✔
NEW
330
                return nil, err
×
NEW
331
        }
×
332

333
        commitID, err := r.Commit(newTreeID, localRef, fmt.Sprintf("Update contents of '%s'\n", localPath), false)
13✔
334
        if err != nil {
13✔
NEW
335
                return nil, err
×
NEW
336
        }
×
337

338
        if !r.IsBare() {
26✔
339
                head, err := r.GetSymbolicReferenceTarget("HEAD")
13✔
340
                if err != nil {
13✔
NEW
341
                        return nil, err
×
NEW
342
                }
×
343
                if head == localRef {
14✔
344
                        worktree := strings.TrimSuffix(r.gitDirPath, ".git") // TODO: this doesn't support detached git dir
1✔
345
                        cwd, err := os.Getwd()
1✔
346
                        if err != nil {
1✔
NEW
347
                                return nil, err
×
NEW
348
                        }
×
349
                        if err := os.Chdir(worktree); err != nil {
1✔
NEW
350
                                return nil, err
×
NEW
351
                        }
×
352
                        defer os.Chdir(cwd) //nolint:errcheck
1✔
353

1✔
354
                        if _, err := r.executor("restore", "--staged", localPath).executeString(); err != nil {
1✔
NEW
355
                                return nil, err
×
NEW
356
                        }
×
357
                        if _, err := r.executor("restore", localPath).executeString(); err != nil {
1✔
NEW
358
                                return nil, err
×
NEW
359
                        }
×
360
                }
361
        }
362

363
        return commitID, nil
13✔
364
}
365

366
// TreeBuilder is used to create multi-level trees in a repository.  Based on
367
// `buildTreeHelper` in go-git.
368
type TreeBuilder struct {
369
        repo    *Repository
370
        trees   map[string]*entryTree
371
        entries map[string]TreeEntry
372
}
373

374
func NewTreeBuilder(repo *Repository) *TreeBuilder {
73✔
375
        return &TreeBuilder{repo: repo}
73✔
376
}
73✔
377

378
// WriteTreeFromEntries accepts list of TreeEntry representations, and returns
379
// the Git ID of the tree that contains these entries. It constructs the
380
// required intermediate trees.
381
func (t *TreeBuilder) WriteTreeFromEntries(files []TreeEntry) (Hash, error) {
128✔
382
        rootNodeKey := ""
128✔
383
        t.trees = map[string]*entryTree{rootNodeKey: {}}
128✔
384
        t.entries = map[string]TreeEntry{}
128✔
385

128✔
386
        for _, entry := range files {
310✔
387
                t.identifyIntermediates(entry)
182✔
388
        }
182✔
389

390
        return t.writeTrees(rootNodeKey, t.trees[rootNodeKey])
128✔
391
}
392

393
// identifyIntermediates identifies the intermediate trees that must be
394
// constructed for the specified path.
395
func (t *TreeBuilder) identifyIntermediates(entry TreeEntry) {
182✔
396
        parts := strings.Split(entry.getName(), "/")
182✔
397

182✔
398
        var fullPath string
182✔
399
        for _, part := range parts {
443✔
400
                parent := fullPath
261✔
401
                fullPath = path.Join(fullPath, part)
261✔
402

261✔
403
                t.populateTree(parent, fullPath, entry)
261✔
404
        }
261✔
405
}
406

407
// populateTree populates tree and entry information for each tree that must be
408
// created.
409
func (t *TreeBuilder) populateTree(parent, fullPath string, entry TreeEntry) {
261✔
410
        if _, ok := t.trees[fullPath]; ok {
276✔
411
                return
15✔
412
        }
15✔
413

414
        if _, ok := t.entries[fullPath]; ok {
246✔
415
                return
×
416
        }
×
417

418
        var entryObj TreeEntry
246✔
419

246✔
420
        if fullPath == entry.getName() {
426✔
421
                // => This is a leaf node
180✔
422
                // However, gitID _may_ be a tree ID, and we've inserted an existing
180✔
423
                // tree object as a subtree here, we want to support this so that we
180✔
424
                // don't have to recreate trees that already exist
180✔
425

180✔
426
                if err := t.repo.ensureIsTree(entry.getID()); err == nil {
196✔
427
                        // gitID represents tree
16✔
428
                        entryObj = &entryTree{
16✔
429
                                name:          path.Base(fullPath),
16✔
430
                                gitID:         entry.getID(),
16✔
431
                                alreadyExists: true,
16✔
432
                        }
16✔
433
                } else {
180✔
434
                        // gitID is not for a tree
164✔
435
                        entryObj = &entryBlob{
164✔
436
                                name:  path.Base(fullPath),
164✔
437
                                gitID: entry.getID(),
164✔
438
                        }
164✔
439
                }
164✔
440
        } else {
66✔
441
                // => This is an intermediate node, has to be a tree that we must build
66✔
442
                entryObj = &entryTree{
66✔
443
                        name:          path.Base(fullPath),
66✔
444
                        gitID:         ZeroHash,
66✔
445
                        alreadyExists: false,
66✔
446
                }
66✔
447
                t.trees[fullPath] = &entryTree{}
66✔
448
        }
66✔
449

450
        t.trees[parent].entries = append(t.trees[parent].entries, entryObj)
246✔
451
}
452

453
// writeTrees recursively stores each tree that must be created in the
454
// repository's object store. It returns the ID of the tree created at each
455
// invocation.
456
func (t *TreeBuilder) writeTrees(parent string, tree *entryTree) (Hash, error) {
194✔
457
        for i, e := range tree.entries {
440✔
458
                switch e := e.(type) {
246✔
459
                case *entryTree:
82✔
460
                        if e.alreadyExists {
98✔
461
                                // The tree already exists and we don't need to write it again.
16✔
462
                                continue
16✔
463
                        }
464

465
                        p := path.Join(parent, e.name)
66✔
466
                        entryID, err := t.writeTrees(p, t.trees[p])
66✔
467
                        if err != nil {
66✔
468
                                return ZeroHash, err
×
469
                        }
×
470
                        e.gitID = entryID
66✔
471

66✔
472
                        tree.entries[i] = e
66✔
473

474
                case *entryBlob:
164✔
475
                        continue
164✔
476
                }
477
        }
478

479
        return t.writeTree(tree.entries)
194✔
480
}
481

482
// writeTree creates a tree in the repository for the specified entries. It
483
// only supports a typical blob with permission 0o644 and a subtree. This is
484
// because it is only intended for use with gittuf specific metadata and tests.
485
// Generic tree creation is left to invocations of the Git binary by the user.
486
func (t *TreeBuilder) writeTree(entries []TreeEntry) (Hash, error) {
194✔
487
        input := ""
194✔
488
        for _, entry := range entries {
440✔
489
                // this is very opinionated about the modes right now because the plan
246✔
490
                // is to use it for gittuf metadata, which requires regular files and
246✔
491
                // subdirectories
246✔
492
                switch entry := entry.(type) {
246✔
493
                case *entryTree:
82✔
494
                        input += "040000 tree " + entry.gitID.String() + "\t" + entry.name
82✔
495
                case *entryBlob:
164✔
496
                        // TODO: support entryBlob's permissions here
164✔
497
                        input += "100644 blob " + entry.gitID.String() + "\t" + entry.name
164✔
498
                }
499
                input += "\n"
246✔
500
        }
501

502
        stdOut, err := t.repo.executor("mktree").withStdIn(bytes.NewBufferString(input)).executeString()
194✔
503
        if err != nil {
194✔
504
                return ZeroHash, fmt.Errorf("unable to write Git tree: %w", err)
×
505
        }
×
506

507
        treeID, err := NewHash(stdOut)
194✔
508
        if err != nil {
194✔
509
                return ZeroHash, fmt.Errorf("invalid tree ID: %w", err)
×
510
        }
×
511

512
        return treeID, nil
194✔
513
}
514

515
// TreeEntry represents an entry in a Git tree.
516
type TreeEntry interface {
517
        getName() string
518
        getID() Hash
519
}
520

521
// entryTree implements TreeEntry and indicates the entry is for a Git tree.
522
type entryTree struct {
523
        name          string
524
        gitID         Hash
525
        alreadyExists bool
526
        entries       []TreeEntry
527
}
528

529
func (e *entryTree) getName() string {
46✔
530
        return e.name
46✔
531
}
46✔
532

533
func (e *entryTree) getID() Hash {
32✔
534
        return e.gitID
32✔
535
}
32✔
536

537
// NewEntryTree creates a TreeEntry that represents a Git tree. If the tree
538
// doesn't exist, i.e., it must be created, gitID must be set to ZeroHash. The
539
// name must be set to the full path of the tree object.
540
func NewEntryTree(name string, gitID Hash) TreeEntry {
18✔
541
        entry := &entryTree{name: name, gitID: gitID}
18✔
542
        if gitID == nil || !gitID.IsZero() {
36✔
543
                entry.alreadyExists = true
18✔
544
        }
18✔
545
        return entry
18✔
546
}
547

548
// entryBlob implements TreeEntry and indicates the entry is for a Git blob.
549
type entryBlob struct {
550
        name        string
551
        gitID       Hash
552
        permissions os.FileMode //nolint:unused
553
}
554

555
func (e *entryBlob) getName() string {
382✔
556
        return e.name
382✔
557
}
382✔
558

559
func (e *entryBlob) getID() Hash {
328✔
560
        return e.gitID
328✔
561
}
328✔
562

563
// NewEntryBlob creates a TreeEntry that represents a Git blob.
564
func NewEntryBlob(name string, gitID Hash) TreeEntry {
156✔
565
        return &entryBlob{name: name, gitID: gitID, permissions: 0o644}
156✔
566
}
156✔
567

568
// NewEntryBlobWithPermissions creates a TreeEntry that represents a Git blob.
569
// The permissions parameter can be used to set custom permissions.
570
func NewEntryBlobWithPermissions(name string, gitID Hash, permissions os.FileMode) TreeEntry {
×
571
        return &entryBlob{name: name, gitID: gitID, permissions: permissions}
×
572
}
×
573

574
// ensureIsTree is a helper to check that the ID represents a Git tree
575
// object.
576
func (r *Repository) ensureIsTree(treeID Hash) error {
182✔
577
        objType, err := r.executor("cat-file", "-t", treeID.String()).executeString()
182✔
578
        if err != nil {
182✔
579
                return fmt.Errorf("unable to inspect if object is tree: %w", err)
×
580
        } else if objType != "tree" {
347✔
581
                return fmt.Errorf("requested Git ID '%s' is not a tree object", treeID.String())
165✔
582
        }
165✔
583

584
        return nil
17✔
585
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc