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

gittuf / gittuf / 29552153989

17 Jul 2026 03:20AM UTC coverage: 78.033% (+0.2%) from 77.791%
29552153989

push

github

web-flow
Merge pull request #1472 from pjbgf/sha256

feat(sha256): support SHA-256 object format

179 of 220 new or added lines in 11 files covered. (81.36%)

2 existing lines in 2 files now uncovered.

12721 of 16302 relevant lines covered (78.03%)

36.22 hits per line

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

85.57
/pkg/gitinterface/repository.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
        "io"
11
        "log/slog"
12
        "os"
13
        "os/exec"
14
        "path/filepath"
15
        "strings"
16

17
        "github.com/go-git/go-git/v6"
18
        gogitconfig "github.com/go-git/go-git/v6/config"
19
        "github.com/jonboulle/clockwork"
20
)
21

22
const (
23
        binary           = "git"
24
        committerTimeKey = "GIT_COMMITTER_DATE"
25
        authorTimeKey    = "GIT_AUTHOR_DATE"
26
)
27

28
var (
29
        ErrRepositoryPathNotSpecified    = errors.New("repository path not specified")
30
        ErrUnknownObjectFormat           = errors.New("unknown object format")
31
        ErrCompatObjectFormatUnsupported = errors.New("gittuf does not support repositories with extensions.compatObjectFormat enabled")
32
)
33

34
// Repository is a lightweight wrapper around a Git repository. It stores the
35
// location of the repository's GIT_DIR.
36
type Repository struct {
37
        gitDirPath   string
38
        objectFormat ObjectFormat
39
        clock        clockwork.Clock
40
}
41

42
// GetObjectFormat returns the hash algorithm the repository uses for its object
43
// IDs.
44
func (r *Repository) GetObjectFormat() ObjectFormat {
11✔
45
        return r.objectFormat
11✔
46
}
11✔
47

48
// readObjectFormat queries Git for the repository's object format (hash
49
// algorithm).
50
func (r *Repository) readObjectFormat() (ObjectFormat, error) {
149✔
51
        stdOut, err := r.executor("rev-parse", "--show-object-format").executeString()
149✔
52
        if err != nil {
149✔
NEW
53
                return "", fmt.Errorf("unable to read object format: %w", err)
×
NEW
54
        }
×
55

56
        switch format := ObjectFormat(stdOut); format {
149✔
57
        case ObjectFormatSHA1, ObjectFormatSHA256:
149✔
58
                return format, nil
149✔
NEW
59
        default:
×
NEW
60
                return "", fmt.Errorf("%w: %s", ErrUnknownObjectFormat, stdOut)
×
61
        }
62
}
63

64
// ensureNoCompatObjectFormat returns an error if the repository is in dual
65
// hash interop mode (extensions.compatObjectFormat). In that mode Git
66
// maintains both SHA-1 and SHA-256 representations of every object and stores
67
// additional compat signatures under headers gittuf does not process, so
68
// signing and verification results would be unreliable. The config file is
69
// read directly (without invoking Git) because Git builds without compat
70
// support refuse to open such repositories at all.
71
func (r *Repository) ensureNoCompatObjectFormat() error {
17✔
72
        configPath := filepath.Join(r.gitDirPath, "config")
17✔
73
        configFile, err := os.Open(configPath)
17✔
74
        if err != nil {
18✔
75
                return fmt.Errorf("unable to read repository config: %w", err)
1✔
76
        }
1✔
77
        defer configFile.Close() //nolint:errcheck
16✔
78

16✔
79
        gitConfig, err := gogitconfig.ReadConfig(configFile)
16✔
80
        if err != nil {
17✔
81
                return fmt.Errorf("unable to parse repository config: %w", err)
1✔
82
        }
1✔
83
        compatFormat := gitConfig.Raw.Section("extensions").Options.Get("compatObjectFormat")
15✔
84
        if compatFormat != "" {
17✔
85
                return fmt.Errorf("%w: compat object format is set to '%s'", ErrCompatObjectFormatUnsupported, compatFormat)
2✔
86
        }
2✔
87

88
        return nil
13✔
89
}
90

91
func findGitDirPath(startPath string) (string, bool, error) {
13✔
92
        currentPath, err := filepath.Abs(startPath)
13✔
93
        if err != nil {
13✔
NEW
94
                return "", false, err
×
NEW
95
        }
×
96

97
        for {
40✔
98
                gitDirPath := filepath.Join(currentPath, ".git")
27✔
99
                if fileInfo, err := os.Stat(gitDirPath); err == nil {
34✔
100
                        if fileInfo.IsDir() {
12✔
101
                                return gitDirPath, true, nil
5✔
102
                        }
5✔
103

104
                        resolvedGitDirPath, err := readGitDirFile(gitDirPath, currentPath)
2✔
105
                        if err != nil {
3✔
106
                                return "", false, err
1✔
107
                        }
1✔
108
                        return resolvedGitDirPath, true, nil
1✔
109
                } else if !os.IsNotExist(err) {
20✔
NEW
110
                        return "", false, err
×
NEW
111
                }
×
112

113
                if isBareGitDir(currentPath) {
22✔
114
                        return currentPath, true, nil
2✔
115
                }
2✔
116

117
                parentPath := filepath.Dir(currentPath)
18✔
118
                if parentPath == currentPath {
22✔
119
                        return "", false, nil
4✔
120
                }
4✔
121
                currentPath = parentPath
14✔
122
        }
123
}
124

125
func readGitDirFile(gitDirFilePath, worktreePath string) (string, error) {
4✔
126
        contents, err := os.ReadFile(gitDirFilePath)
4✔
127
        if err != nil {
5✔
128
                return "", err
1✔
129
        }
1✔
130

131
        gitDirPath, has := strings.CutPrefix(strings.TrimSpace(string(contents)), "gitdir:")
3✔
132
        if !has {
4✔
133
                return "", fmt.Errorf("invalid gitdir file: %s", gitDirFilePath)
1✔
134
        }
1✔
135
        gitDirPath = strings.TrimSpace(gitDirPath)
2✔
136
        if !filepath.IsAbs(gitDirPath) {
3✔
137
                gitDirPath = filepath.Join(worktreePath, gitDirPath)
1✔
138
        }
1✔
139

140
        return filepath.Abs(gitDirPath)
2✔
141
}
142

143
func isBareGitDir(path string) bool {
22✔
144
        if fileInfo, err := os.Stat(filepath.Join(path, "config")); err != nil || fileInfo.IsDir() {
40✔
145
                return false
18✔
146
        }
18✔
147
        if fileInfo, err := os.Stat(filepath.Join(path, "HEAD")); err != nil || fileInfo.IsDir() {
6✔
148
                return false
2✔
149
        }
2✔
150
        return true
2✔
151
}
152

153
// GetGoGitRepository returns the go-git representation of a repository. We use
154
// this in certain signing and verifying workflows.
155
func (r *Repository) GetGoGitRepository() (*git.Repository, error) {
49✔
156
        return git.PlainOpenWithOptions(r.gitDirPath, &git.PlainOpenOptions{DetectDotGit: true})
49✔
157
}
49✔
158

159
// GetGitDir returns the GIT_DIR path for the repository.
160
func (r *Repository) GetGitDir() string {
2✔
161
        return r.gitDirPath
2✔
162
}
2✔
163

164
// IsBare returns true if the repository is a bare repository.
165
func (r *Repository) IsBare() bool {
55✔
166
        // TODO: this may not work when the repo is cloned with GIT_DIR set
55✔
167
        // elsewhere. We don't support this at the moment, so it's probably okay?
55✔
168
        return !strings.HasSuffix(r.gitDirPath, ".git")
55✔
169
}
55✔
170

171
// LoadRepository returns a Repository instance using the current working
172
// directory. It also inspects the PATH to ensure Git is installed.
173
func LoadRepository(repositoryPath string) (*Repository, error) {
7✔
174
        slog.Debug("Looking for Git binary in PATH...")
7✔
175
        _, err := exec.LookPath(binary)
7✔
176
        if err != nil {
7✔
177
                return nil, fmt.Errorf("unable to find Git binary, is Git installed?")
×
178
        }
×
179
        if repositoryPath == "" {
8✔
180
                return nil, ErrRepositoryPathNotSpecified
1✔
181
        }
1✔
182

183
        repo := &Repository{clock: clockwork.NewRealClock()}
6✔
184
        currentDir, err := os.Getwd()
6✔
185
        if err != nil {
6✔
186
                return nil, err
×
187
        }
×
188

189
        if err = os.Chdir(repositoryPath); err != nil {
6✔
190
                return nil, err
×
191
        }
×
192
        defer os.Chdir(currentDir) //nolint:errcheck
6✔
193

6✔
194
        gitDirPath, has, err := findGitDirPath(".")
6✔
195
        if err != nil {
6✔
NEW
196
                return nil, err
×
NEW
197
        }
×
198
        if has {
11✔
199
                repo.gitDirPath = gitDirPath
5✔
200
                if err := repo.ensureNoCompatObjectFormat(); err != nil {
6✔
201
                        return nil, err
1✔
202
                }
1✔
203
        }
204

205
        slog.Debug("Identifying git directory for repository...")
5✔
206
        stdOut, stdErr, err := repo.executor("rev-parse", "--git-dir").withoutGitDir().execute()
5✔
207
        if err != nil {
6✔
208
                errContents, newErr := io.ReadAll(stdErr)
1✔
209
                if newErr != nil {
1✔
210
                        return nil, fmt.Errorf("unable to read original err '%w' when loading repository: %w", err, newErr)
×
211
                }
×
212
                return nil, fmt.Errorf("unable to identify git directory for repository: %w: %s", err, strings.TrimSpace(string(errContents)))
1✔
213
        }
214

215
        stdOutContents, err := io.ReadAll(stdOut)
4✔
216
        if err != nil {
4✔
217
                return nil, fmt.Errorf("unable to identify git directory for repository: %w", err)
×
218
        }
×
219

220
        // git rev-parse --git-dir returns a local path, so filepath.Abs gives us
221
        // the final path _including_ symlink follows.
222
        absPath, err := filepath.Abs(strings.TrimSpace(string(stdOutContents)))
4✔
223
        if err != nil {
4✔
224
                return nil, err
×
225
        }
×
226
        slog.Debug(fmt.Sprintf("Setting git directory for repository to '%s'...", absPath))
4✔
227
        repo.gitDirPath = absPath
4✔
228

4✔
229
        objectFormat, err := repo.readObjectFormat()
4✔
230
        if err != nil {
4✔
NEW
231
                return nil, err
×
NEW
232
        }
×
233
        repo.objectFormat = objectFormat
4✔
234

4✔
235
        return repo, nil
4✔
236
}
237

238
// executor is a lightweight wrapper around exec.Cmd to run Git commands. It
239
// accepts the arguments to the `git` binary, but the binary itself must not be
240
// specified.
241
type executor struct {
242
        r           *Repository
243
        args        []string
244
        env         []string
245
        stdIn       io.Reader
246
        unsetGitDir bool
247
}
248

249
// executor initializes a new executor instance to run a Git command with the
250
// specified arguments.
251
func (r *Repository) executor(args ...string) *executor {
2,888✔
252
        return &executor{r: r, args: args, env: os.Environ()}
2,888✔
253
}
2,888✔
254

255
// withEnv adds the specified environment variables. Each environment variable
256
// must be specified in the form of `key=value`.
257
func (e *executor) withEnv(env ...string) *executor {
204✔
258
        e.env = append(e.env, env...)
204✔
259
        return e
204✔
260
}
204✔
261

262
// withoutGitDir ensures the executor doesn't auto-set the --git-dir flag to the
263
// executed command.
264
func (e *executor) withoutGitDir() *executor {
5✔
265
        e.unsetGitDir = true
5✔
266
        return e
5✔
267
}
5✔
268

269
// withStdIn sets the contents of stdin to be passed in to the command.
270
func (e *executor) withStdIn(stdIn *bytes.Buffer) *executor {
364✔
271
        e.stdIn = stdIn
364✔
272
        return e
364✔
273
}
364✔
274

275
// executeString runs the constructed Git command and returns the contents of
276
// stdout.  Leading and trailing spaces and newlines are removed. This function
277
// should be used almost every time; the only exception is when the output is
278
// desired without any processing such as the removal of space characters.
279
func (e *executor) executeString() (string, error) {
2,873✔
280
        stdOut, stdErr, err := e.execute()
2,873✔
281
        if err != nil {
3,087✔
282
                stdErrContents, newErr := io.ReadAll(stdErr)
214✔
283
                if newErr != nil {
214✔
284
                        return "", fmt.Errorf("unable to read stderr contents: %w; original err: %w", newErr, err)
×
285
                }
×
286
                return "", fmt.Errorf("%w when executing `git %s`: %s", err, strings.Join(e.args, " "), string(stdErrContents))
214✔
287
        }
288

289
        stdOutContents, err := io.ReadAll(stdOut)
2,659✔
290
        if err != nil {
2,659✔
291
                return "", fmt.Errorf("unable to read stdout contents: %w", err)
×
292
        }
×
293

294
        return strings.TrimSpace(string(stdOutContents)), nil
2,659✔
295
}
296

297
// execute runs the constructed Git command and returns the raw stdout and
298
// stderr contents. It adds the `--git-dir` argument if the repository has a
299
// path set.
300
func (e *executor) execute() (io.Reader, io.Reader, error) {
2,888✔
301
        if e.r.gitDirPath != "" && !e.unsetGitDir {
5,771✔
302
                e.args = append([]string{"--git-dir", e.r.gitDirPath}, e.args...)
2,883✔
303
        }
2,883✔
304
        cmd := exec.Command(binary, e.args...) //nolint:gosec
2,888✔
305
        cmd.Env = e.env
2,888✔
306
        cmd.Env = append(cmd.Env, "LC_ALL=C") // force git to the C (and thus english) locale
2,888✔
307

2,888✔
308
        var (
2,888✔
309
                stdOut bytes.Buffer
2,888✔
310
                stdErr bytes.Buffer
2,888✔
311
        )
2,888✔
312

2,888✔
313
        cmd.Stdout = &stdOut
2,888✔
314
        cmd.Stderr = &stdErr
2,888✔
315

2,888✔
316
        if e.stdIn != nil {
3,252✔
317
                cmd.Stdin = e.stdIn
364✔
318
        }
364✔
319

320
        err := cmd.Run()
2,888✔
321

2,888✔
322
        return &stdOut, &stdErr, err
2,888✔
323
}
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