• 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

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

4
package gitinterface
5

6
import (
7
        "errors"
8
        "fmt"
9
        "strings"
10
)
11

12
// See https://git-scm.com/docs/git-status#_porcelain_format_version_1.
13

14
var (
15
        ErrInvalidStatusCodeLength = errors.New("status code string must be of length 1")
16
        ErrInvalidStatusCode       = errors.New("status code string is unrecognized")
17
)
18

19
type StatusCode uint
20

21
const (
22
        StatusCodeUnmodified StatusCode = iota + 1 // we use 0 as error code
23
        StatusCodeModified
24
        StatusCodeTypeChanged
25
        StatusCodeAdded
26
        StatusCodeDeleted
27
        StatusCodeRenamed
28
        StatusCodeCopied
29
        StatusCodeUpdatedUnmerged
30
        StatusCodeUntracked
31
        StatusCodeIgnored
32
)
33

34
func (s StatusCode) String() string {
11✔
35
        switch s {
11✔
36
        case StatusCodeUnmodified:
1✔
37
                return " " // is this actually a space or empty string?
1✔
38
        case StatusCodeModified:
1✔
39
                return "M"
1✔
40
        case StatusCodeTypeChanged:
1✔
41
                return "T"
1✔
42
        case StatusCodeAdded:
1✔
43
                return "A"
1✔
44
        case StatusCodeDeleted:
1✔
45
                return "D"
1✔
46
        case StatusCodeRenamed:
1✔
47
                return "R"
1✔
48
        case StatusCodeCopied:
1✔
49
                return "C"
1✔
50
        case StatusCodeUpdatedUnmerged:
1✔
51
                return "U"
1✔
52
        case StatusCodeUntracked:
1✔
53
                return "?"
1✔
54
        case StatusCodeIgnored:
1✔
55
                return "!"
1✔
56
        default:
1✔
57
                return "invalid-code"
1✔
58
        }
59
}
60

61
func NewStatusCodeFromByte(s byte) (StatusCode, error) {
28✔
62
        switch s {
28✔
63
        case ' ':
8✔
64
                return StatusCodeUnmodified, nil
8✔
65
        case 'M':
3✔
66
                return StatusCodeModified, nil
3✔
67
        case 'T':
2✔
68
                return StatusCodeTypeChanged, nil
2✔
69
        case 'A':
4✔
70
                return StatusCodeAdded, nil
4✔
71
        case 'D':
2✔
72
                return StatusCodeDeleted, nil
2✔
73
        case 'R':
2✔
74
                return StatusCodeRenamed, nil
2✔
75
        case 'C':
1✔
76
                return StatusCodeCopied, nil
1✔
77
        case 'U':
1✔
78
                return StatusCodeUpdatedUnmerged, nil
1✔
79
        case '?':
3✔
80
                return StatusCodeUntracked, nil
3✔
81
        case '!':
1✔
82
                return StatusCodeIgnored, nil
1✔
83
        default:
1✔
84
                return 0, ErrInvalidStatusCode
1✔
85
        }
86
}
87

88
type FileStatus struct {
89
        X StatusCode
90
        Y StatusCode
91
}
92

93
func (f *FileStatus) Untracked() bool {
3✔
94
        return f.X == StatusCodeUntracked || f.Y == StatusCodeUntracked
3✔
95
}
3✔
96

97
func (r *Repository) Status() (map[string]FileStatus, error) {
15✔
98
        worktree := r.gitDirPath
15✔
99
        if !r.IsBare() {
29✔
100
                worktree = strings.TrimSuffix(worktree, ".git") // TODO: this doesn't support detached git dir
14✔
101
        }
14✔
102

103
        output, err := r.executor("status", "--porcelain=1", "-z", "--untracked-files=all", "--ignored").withDir(worktree).executeString()
15✔
104
        if err != nil {
16✔
105
                return nil, fmt.Errorf("unable to check status of repository: %w", err)
1✔
106
        }
1✔
107

108
        statuses := map[string]FileStatus{}
14✔
109

14✔
110
        // `git status --porcelain=1 -z` emits NUL-separated tokens.
14✔
111
        // For rename/copy records, the source path is emitted as an additional
14✔
112
        // token after the main status token.
14✔
113
        tokens := strings.Split(output, string('\000'))
14✔
114
        for i := 0; i < len(tokens); i++ {
37✔
115
                token := tokens[i]
23✔
116
                if len(token) == 0 {
37✔
117
                        continue
14✔
118
                }
119

120
                // first two characters are status codes, find the corresponding
121
                // statuses
122
                xb := token[0]
9✔
123
                yb := token[1]
9✔
124
                // Note: we identify the status after inspecting the path so we can
9✔
125
                // provide better error messages
9✔
126

9✔
127
                // then, we have a single space followed by the path, ignore space and
9✔
128
                // read in the rest as the filepath
9✔
129
                filePath := strings.TrimSpace(token[2:])
9✔
130

9✔
131
                xStatus, err := NewStatusCodeFromByte(xb)
9✔
132
                if err != nil {
9✔
UNCOV
133
                        return nil, fmt.Errorf("unable to parse status code '%c' for path '%s': %w", xb, filePath, err)
×
UNCOV
134
                }
×
135

136
                yStatus, err := NewStatusCodeFromByte(yb)
9✔
137
                if err != nil {
9✔
UNCOV
138
                        return nil, fmt.Errorf("unable to parse status code '%c' for path '%s': %w", yb, filePath, err)
×
UNCOV
139
                }
×
140

141
                status := FileStatus{X: xStatus, Y: yStatus}
9✔
142

9✔
143
                statuses[filePath] = status
9✔
144

9✔
145
                // After splitting on NUL, rename/copy records have an additional token
9✔
146
                // for the source path immediately after the main token.
9✔
147
                if xStatus == StatusCodeRenamed || xStatus == StatusCodeCopied ||
9✔
148
                        yStatus == StatusCodeRenamed || yStatus == StatusCodeCopied {
11✔
149
                        if i+1 >= len(tokens) || len(tokens[i+1]) == 0 {
2✔
UNCOV
150
                                return nil, fmt.Errorf("unable to parse rename/copy status for path '%s': missing source path", filePath)
×
UNCOV
151
                        }
×
152
                        i++
2✔
153
                }
154
        }
155

156
        return statuses, nil
14✔
157
}
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