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

gittuf / gittuf / 30044417887

23 Jul 2026 08:59PM UTC coverage: 78.204% (+0.06%) from 78.143%
30044417887

push

github

web-flow
Merge pull request #1500 from gittuf/fix-tests

pkg: Fix gitinterface test

1 of 1 new or added line in 1 file covered. (100.0%)

31 existing lines in 4 files now uncovered.

12834 of 16411 relevant lines covered (78.2%)

36.49 hits per line

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

87.76
/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) {
194✔
51
        stdOut, err := r.executor("rev-parse", "--show-object-format").executeString()
194✔
52
        if err != nil {
194✔
53
                return "", fmt.Errorf("unable to read object format: %w", err)
×
54
        }
×
55

56
        switch format := ObjectFormat(stdOut); format {
194✔
57
        case ObjectFormatSHA1, ObjectFormatSHA256:
194✔
58
                return format, nil
194✔
59
        default:
×
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 {
57✔
72
        configPath := filepath.Join(r.gitDirPath, "config")
57✔
73
        configFile, err := os.Open(configPath)
57✔
74
        if err != nil {
58✔
75
                return fmt.Errorf("unable to read repository config: %w", err)
1✔
76
        }
1✔
77
        defer configFile.Close() //nolint:errcheck
56✔
78

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

88
        return nil
53✔
89
}
90

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

97
        for {
120✔
98
                gitDirPath := filepath.Join(currentPath, ".git")
67✔
99
                if fileInfo, err := os.Stat(gitDirPath); err == nil {
94✔
100
                        if fileInfo.IsDir() {
52✔
101
                                return gitDirPath, true, nil
25✔
102
                        }
25✔
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) {
40✔
110
                        return "", false, err
×
111
                }
×
112

113
                if isBareGitDir(currentPath) {
62✔
114
                        return currentPath, true, nil
22✔
115
                }
22✔
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 {
42✔
144
        if fileInfo, err := os.Stat(filepath.Join(path, "config")); err != nil || fileInfo.IsDir() {
60✔
145
                return false
18✔
146
        }
18✔
147
        if fileInfo, err := os.Stat(filepath.Join(path, "HEAD")); err != nil || fileInfo.IsDir() {
26✔
148
                return false
2✔
149
        }
2✔
150
        return true
22✔
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) {
52✔
156
        // gitDirPath is already the resolved git directory (set via
52✔
157
        // `git rev-parse --git-dir` in LoadRepository), so DetectDotGit must be
52✔
158
        // false: with it true, go-git looks for a .git entry inside this path,
52✔
159
        // which a bare repository does not have, and returns ErrRepositoryNotExists.
52✔
160
        return git.PlainOpenWithOptions(r.gitDirPath, &git.PlainOpenOptions{DetectDotGit: false})
52✔
161
}
52✔
162

163
// GetGitDir returns the GIT_DIR path for the repository.
164
func (r *Repository) GetGitDir() string {
42✔
165
        return r.gitDirPath
42✔
166
}
42✔
167

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

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

187
        repo := &Repository{clock: clockwork.NewRealClock()}
46✔
188

46✔
189
        gitDirPath, has, err := findGitDirPath(repositoryPath)
46✔
190
        if err != nil {
46✔
191
                return nil, err
×
UNCOV
192
        }
×
193
        if has {
91✔
194
                repo.gitDirPath = gitDirPath
45✔
195
                if err := repo.ensureNoCompatObjectFormat(); err != nil {
46✔
196
                        return nil, err
1✔
197
                }
1✔
198
        }
199

200
        slog.Debug("Identifying git directory for repository...")
45✔
201
        stdOut, stdErr, err := repo.executor("rev-parse", "--absolute-git-dir").withoutGitDir().withDir(repositoryPath).execute()
45✔
202
        if err != nil {
46✔
203
                errContents, newErr := io.ReadAll(stdErr)
1✔
204
                if newErr != nil {
1✔
UNCOV
205
                        return nil, fmt.Errorf("unable to read original err '%w' when loading repository: %w", err, newErr)
×
UNCOV
206
                }
×
207
                return nil, fmt.Errorf("unable to identify git directory for repository: %w: %s", err, strings.TrimSpace(string(errContents)))
1✔
208
        }
209

210
        stdOutContents, err := io.ReadAll(stdOut)
44✔
211
        if err != nil {
44✔
UNCOV
212
                return nil, fmt.Errorf("unable to identify git directory for repository: %w", err)
×
UNCOV
213
        }
×
214

215
        absPath, err := filepath.EvalSymlinks(strings.TrimSpace(string(stdOutContents)))
44✔
216
        if err != nil {
44✔
UNCOV
217
                return nil, err
×
UNCOV
218
        }
×
219
        slog.Debug(fmt.Sprintf("Setting git directory for repository to '%s'...", absPath))
44✔
220
        repo.gitDirPath = absPath
44✔
221

44✔
222
        objectFormat, err := repo.readObjectFormat()
44✔
223
        if err != nil {
44✔
UNCOV
224
                return nil, err
×
UNCOV
225
        }
×
226
        repo.objectFormat = objectFormat
44✔
227

44✔
228
        return repo, nil
44✔
229
}
230

231
// executor is a lightweight wrapper around exec.Cmd to run Git commands. It
232
// accepts the arguments to the `git` binary, but the binary itself must not be
233
// specified.
234
type executor struct {
235
        r           *Repository
236
        args        []string
237
        env         []string
238
        stdIn       io.Reader
239
        dir         string
240
        unsetGitDir bool
241
}
242

243
// executor initializes a new executor instance to run a Git command with the
244
// specified arguments.
245
func (r *Repository) executor(args ...string) *executor {
2,997✔
246
        return &executor{r: r, args: args, env: os.Environ()}
2,997✔
247
}
2,997✔
248

249
// withEnv adds the specified environment variables. Each environment variable
250
// must be specified in the form of `key=value`.
251
func (e *executor) withEnv(env ...string) *executor {
204✔
252
        e.env = append(e.env, env...)
204✔
253
        return e
204✔
254
}
204✔
255

256
// withoutGitDir ensures the executor doesn't auto-set the --git-dir flag to the
257
// executed command.
258
func (e *executor) withoutGitDir() *executor {
45✔
259
        e.unsetGitDir = true
45✔
260
        return e
45✔
261
}
45✔
262

263
// withDir runs the command with the given working directory instead of the
264
// process's. Use this for worktree-relative commands (status, restore) so the
265
// process-global os.Chdir is never touched.
266
func (e *executor) withDir(dir string) *executor {
64✔
267
        e.dir = dir
64✔
268
        return e
64✔
269
}
64✔
270

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

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

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

296
        return strings.TrimSpace(string(stdOutContents)), nil
2,727✔
297
}
298

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

313
        var (
2,997✔
314
                stdOut bytes.Buffer
2,997✔
315
                stdErr bytes.Buffer
2,997✔
316
        )
2,997✔
317

2,997✔
318
        cmd.Stdout = &stdOut
2,997✔
319
        cmd.Stderr = &stdErr
2,997✔
320

2,997✔
321
        if e.stdIn != nil {
3,363✔
322
                cmd.Stdin = e.stdIn
366✔
323
        }
366✔
324

325
        err := cmd.Run()
2,997✔
326

2,997✔
327
        return &stdOut, &stdErr, err
2,997✔
328
}
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