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

murex / TCR / 18999741724

01 Nov 2025 04:50PM UTC coverage: 89.694% (-0.07%) from 89.763%
18999741724

push

github

mengdaming
Update dependencies

5274 of 5880 relevant lines covered (89.69%)

1062.76 hits per line

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

81.01
/src/vcs/git/git_impl.go
1
/*
2
Copyright (c) 2022 Murex
3

4
Permission is hereby granted, free of charge, to any person obtaining a copy
5
of this software and associated documentation files (the "Software"), to deal
6
in the Software without restriction, including without limitation the rights
7
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
copies of the Software, and to permit persons to whom the Software is
9
furnished to do so, subject to the following conditions:
10

11
The above copyright notice and this permission notice shall be included in all
12
copies or substantial portions of the Software.
13

14
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
SOFTWARE.
21
*/
22

23
package git
24

25
import (
26
        "bufio"
27
        "bytes"
28
        "errors"
29
        "fmt"
30
        "path/filepath"
31
        "slices"
32
        "strconv"
33
        "strings"
34

35
        "github.com/go-git/go-billy/v5"
36
        "github.com/go-git/go-git/v5"
37
        "github.com/go-git/go-git/v5/plumbing"
38
        "github.com/go-git/go-git/v5/plumbing/object"
39
        "github.com/go-git/go-git/v5/plumbing/storer"
40
        "github.com/go-git/go-git/v5/storage/filesystem"
41
        "github.com/murex/tcr/report"
42
        "github.com/murex/tcr/vcs"
43
)
44

45
// Name provides the name for this VCS implementation
46
const Name = "git"
47

48
// gitImpl provides the implementation of the git interface
49
type gitImpl struct {
50
        baseDir                     string
51
        rootDir                     string
52
        repository                  *git.Repository
53
        filesystem                  billy.Filesystem
54
        remoteName                  string
55
        remoteEnabled               bool
56
        workingBranch               string
57
        workingBranchExistsOnRemote bool
58
        autoPushEnabled             bool
59
        runGitFunction              func(params ...string) (output []byte, err error)
60
        traceGitFunction            func(params ...string) (err error)
61
}
62

63
// New initializes the git implementation based on the provided directory from local clone
64
func New(dir string, remoteName string) (vcs.Interface, error) {
6✔
65
        return newGitImpl(plainOpen, dir, remoteName)
6✔
66
}
6✔
67

68
func newGitImpl(
69
        initRepo func(string) (*git.Repository, billy.Filesystem, error),
70
        dir string,
71
        remoteName string,
72
) (*gitImpl, error) {
180✔
73
        g := gitImpl{
180✔
74
                baseDir:          dir,
180✔
75
                remoteName:       remoteName,
180✔
76
                autoPushEnabled:  vcs.DefaultAutoPushEnabled,
180✔
77
                runGitFunction:   runGitCommand,
180✔
78
                traceGitFunction: traceGitCommand,
180✔
79
        }
180✔
80

180✔
81
        var err error
180✔
82
        g.repository, g.filesystem, err = initRepo(dir)
180✔
83
        if err != nil {
183✔
84
                return nil, fmt.Errorf("%s %s", Name, err.Error())
3✔
85
        }
3✔
86

87
        g.rootDir = retrieveRootDir(g.filesystem)
177✔
88

177✔
89
        g.workingBranch, err = retrieveWorkingBranch(g.repository)
177✔
90
        if err != nil {
177✔
91
                return nil, fmt.Errorf("%s %s", Name, err.Error())
×
92
        }
×
93

94
        if isRemoteDefined(remoteName, g.repository) {
177✔
95
                report.PostInfo("Git remote name is ", g.remoteName)
×
96
                g.remoteEnabled = true
×
97
                g.workingBranchExistsOnRemote, err = g.isWorkingBranchOnRemote()
×
98
                if err != nil {
×
99
                        return nil, fmt.Errorf("%s %s", Name, err.Error())
×
100
                }
×
101
        } else {
177✔
102
                report.PostWarning("Git remote name not found: ", g.remoteName)
177✔
103
        }
177✔
104

105
        return &g, nil
177✔
106
}
107

108
// Name returns VCS name
109
func (*gitImpl) Name() string {
15✔
110
        return Name
15✔
111
}
15✔
112

113
// SessionSummary provides a short description related to current VCS session summary
114
func (g *gitImpl) SessionSummary() string {
12✔
115
        branch := g.GetWorkingBranch()
12✔
116
        if g.IsRemoteEnabled() {
18✔
117
                branch = fmt.Sprintf("%s/%s", g.GetRemoteName(), g.GetWorkingBranch())
6✔
118
        }
6✔
119
        return fmt.Sprintf("%s branch \"%s\"", g.Name(), branch)
12✔
120
}
121

122
// plainOpen is the regular function used to open a repository
123
func plainOpen(dir string) (*git.Repository, billy.Filesystem, error) {
6✔
124
        repo, err := git.PlainOpenWithOptions(dir, &git.PlainOpenOptions{
6✔
125
                DetectDotGit:          true,
6✔
126
                EnableDotGitCommonDir: false,
6✔
127
        })
6✔
128
        if err != nil {
9✔
129
                return nil, nil, err
3✔
130
        }
3✔
131

132
        // Try to grab the repository Storer
133
        storage, ok := repo.Storer.(*filesystem.Storage)
3✔
134
        if !ok {
3✔
135
                return nil, nil, errors.New("repository storage is not filesystem.Storage")
×
136
        }
×
137
        return repo, storage.Filesystem(), nil
3✔
138
}
139

140
// isRemoteDefined returns true is the provided remote is defined in the repository
141
func isRemoteDefined(remoteName string, repo *git.Repository) bool {
177✔
142
        if remoteName == "" {
345✔
143
                return false
168✔
144
        }
168✔
145
        _, err := repo.Remote(remoteName)
9✔
146
        return err == nil
9✔
147
}
148

149
// isWorkingBranchOnRemote returns true is the working branch exists on remote repository
150
func (g *gitImpl) isWorkingBranchOnRemote() (onRemote bool, err error) {
×
151
        var branches storer.ReferenceIter
×
152
        branches, err = remoteBranches(g.repository.Storer)
×
153
        if err != nil {
×
154
                return false, err
×
155
        }
×
156

157
        remoteBranchName := fmt.Sprintf("%v/%v", g.GetRemoteName(), g.GetWorkingBranch())
×
158
        _ = branches.ForEach(func(branch *plumbing.Reference) error {
×
159
                onRemote = onRemote || strings.HasSuffix(branch.Name().Short(), remoteBranchName)
×
160
                return nil
×
161
        })
×
162
        return onRemote, err
×
163
}
164

165
// remoteBranches returns the list of known remote branches
166
func remoteBranches(s storer.ReferenceStorer) (storer.ReferenceIter, error) {
×
167
        refs, err := s.IterReferences()
×
168
        if err != nil {
×
169
                return nil, err
×
170
        }
×
171

172
        // We keep only remote branches, and ignore symbolic references
173
        return storer.NewReferenceFilteredIter(func(ref *plumbing.Reference) bool {
×
174
                return ref.Name().IsRemote() && ref.Type() != plumbing.SymbolicReference
×
175
        }, refs), nil
×
176
}
177

178
// retrieveRootDir returns the local clone's root directory of provided repository
179
func retrieveRootDir(fs billy.Filesystem) string {
177✔
180
        return filepath.Dir(fs.Root())
177✔
181
}
177✔
182

183
// retrieveWorkingBranch returns the current working branch for provided repository
184
func retrieveWorkingBranch(repository *git.Repository) (string, error) {
177✔
185
        // Repo with at least one commit
177✔
186
        head, err := repository.Head()
177✔
187
        if err == nil {
180✔
188
                return head.Name().Short(), nil
3✔
189
        }
3✔
190

191
        // Brand-new repo: nothing is committed yet
192
        head, err = repository.Reference(plumbing.HEAD, false)
174✔
193
        if err != nil {
174✔
194
                return "", err
×
195
        }
×
196
        return head.Target().Short(), nil
174✔
197
}
198

199
// GetRootDir returns the root directory path
200
func (g *gitImpl) GetRootDir() string {
93✔
201
        return g.rootDir
93✔
202
}
93✔
203

204
// GetRemoteName returns the current git remote name
205
func (g *gitImpl) GetRemoteName() string {
48✔
206
        return g.remoteName
48✔
207
}
48✔
208

209
// IsRemoteEnabled indicates if git remote operations are enabled
210
func (g *gitImpl) IsRemoteEnabled() bool {
39✔
211
        return g.remoteEnabled
39✔
212
}
39✔
213

214
// GetWorkingBranch returns the current git working branch
215
func (g *gitImpl) GetWorkingBranch() string {
84✔
216
        return g.workingBranch
84✔
217
}
84✔
218

219
// IsOnRootBranch indicates if git is currently on its root branch or not.
220
// Current implementation is a trivial one, that returns true if the branch is called "main" or "master"
221
func (g *gitImpl) IsOnRootBranch() bool {
12✔
222
        return slices.Contains([]string{"main", "master"}, g.GetWorkingBranch())
12✔
223
}
12✔
224

225
// Add adds the listed paths to git index.
226
// Current implementation uses a direct call to git
227
func (g *gitImpl) Add(paths ...string) error {
12✔
228
        gitArgs := []string{"add"}
12✔
229
        if len(paths) == 0 {
15✔
230
                gitArgs = append(gitArgs, ".")
3✔
231
        } else {
12✔
232
                gitArgs = append(gitArgs, paths...)
9✔
233
        }
9✔
234
        return g.traceGit(gitArgs...)
12✔
235
}
236

237
// Commit commits changes to git index.
238
// Current implementation uses a direct call to git
239
func (g *gitImpl) Commit(messages ...string) error {
12✔
240
        gitArgs := []string{"commit", "--no-gpg-sign"}
12✔
241
        for _, message := range messages {
30✔
242
                gitArgs = append(gitArgs, "-m", message)
18✔
243
        }
18✔
244
        err := g.traceGit(gitArgs...)
12✔
245
        // This is to prevent from returning an error when there is nothing to commit
12✔
246
        if err != nil && g.nothingToCommit() {
15✔
247
                return nil
3✔
248
        }
3✔
249
        return err
9✔
250
}
251

252
// nothingToCommit returns true if there is nothing to commit
253
func (g *gitImpl) nothingToCommit() bool {
18✔
254
        worktree, _ := g.repository.Worktree()
18✔
255
        status, err := worktree.Status()
18✔
256
        if err != nil {
18✔
257
                return false
×
258
        }
×
259
        return status.IsClean()
18✔
260
}
261

262
// RevertLocal restores to last commit for the provided path.
263
// Current implementation uses a direct call to git
264
func (g *gitImpl) RevertLocal(path string) error {
6✔
265
        report.PostWarning("Reverting ", path)
6✔
266
        return g.traceGit("checkout", "HEAD", "--", path)
6✔
267
}
6✔
268

269
// RollbackLastCommit runs a git revert operation.
270
// Current implementation uses a direct call to git
271
func (g *gitImpl) RollbackLastCommit() error {
6✔
272
        report.PostInfo("Reverting changes")
6✔
273
        return g.traceGit("revert", "--no-gpg-sign", "--no-edit", "--no-commit", "HEAD")
6✔
274
}
6✔
275

276
// Push runs a git push operation.
277
// Current implementation uses a direct call to git
278
func (g *gitImpl) Push() error {
12✔
279
        if !g.IsRemoteEnabled() {
12✔
280
                // There's nothing to do in this case
×
281
                return nil
×
282
        }
×
283

284
        report.PostInfo("Pushing changes to ", g.GetRemoteName(), "/", g.GetWorkingBranch())
12✔
285
        err := g.traceGit("push", "--no-recurse-submodules", g.GetRemoteName(), g.GetWorkingBranch())
12✔
286
        if err == nil {
18✔
287
                g.workingBranchExistsOnRemote = true
6✔
288
        }
6✔
289
        return err
12✔
290
}
291

292
// Pull runs a git pull operation.
293
// Current implementation uses a direct call to git
294
func (g *gitImpl) Pull() error {
12✔
295
        if !g.workingBranchExistsOnRemote || !g.IsRemoteEnabled() {
18✔
296
                report.PostInfo("Working locally on branch ", g.GetWorkingBranch())
6✔
297
                return nil
6✔
298
        }
6✔
299
        report.PostInfo("Pulling latest changes from ", g.GetRemoteName(), "/", g.GetWorkingBranch())
6✔
300
        return g.traceGit("pull", "--no-recurse-submodules", g.GetRemoteName(), g.GetWorkingBranch())
6✔
301
}
302

303
// Diff returns the list of files modified since last commit with diff info for each file
304
// Current implementation uses a direct call to git
305
func (g *gitImpl) Diff() (diffs vcs.FileDiffs, err error) {
27✔
306
        var gitOutput []byte
27✔
307
        gitOutput, err = g.runGit("diff", "--numstat", "--ignore-cr-at-eol",
27✔
308
                "--ignore-blank-lines", "HEAD")
27✔
309
        if err != nil {
30✔
310
                return nil, err
3✔
311
        }
3✔
312

313
        scanner := bufio.NewScanner(bytes.NewReader(gitOutput))
24✔
314
        for scanner.Scan() {
51✔
315
                fields := strings.Split(scanner.Text(), "\t")
27✔
316
                if len(fields) == 3 { //nolint:revive
48✔
317
                        added, _ := strconv.Atoi(fields[0])
21✔
318
                        removed, _ := strconv.Atoi(fields[1])
21✔
319
                        filename := filepath.Join(g.rootDir, fields[2])
21✔
320
                        diffs = append(diffs, vcs.NewFileDiff(filename, added, removed))
21✔
321
                }
21✔
322
        }
323
        return diffs, nil
24✔
324
}
325

326
// Log returns the list of git log items compliant with the provided msgFilter.
327
// When no msgFilter is provided, returns all git log items unfiltered.
328
// Current implementation uses go-git's Log() function
329
func (g *gitImpl) Log(msgFilter func(msg string) bool) (logs vcs.LogItems, err error) {
9✔
330
        plainOpenOptions := git.PlainOpenOptions{
9✔
331
                DetectDotGit:          true,
9✔
332
                EnableDotGitCommonDir: false,
9✔
333
        }
9✔
334
        var repo *git.Repository
9✔
335
        repo, err = git.PlainOpenWithOptions(g.baseDir, &plainOpenOptions)
9✔
336
        if err != nil {
9✔
337
                return nil, err
×
338
        }
×
339
        var head *plumbing.Reference
9✔
340
        head, err = repo.Head()
9✔
341
        if err != nil {
9✔
342
                return nil, err
×
343
        }
×
344

345
        var cIter object.CommitIter
9✔
346
        cIter, err = repo.Log(&git.LogOptions{From: head.Hash()})
9✔
347
        if err != nil {
9✔
348
                return nil, err
×
349
        }
×
350
        _ = cIter.ForEach(func(c *object.Commit) error {
22,041✔
351
                if msgFilter == nil || msgFilter(c.Message) {
29,379✔
352
                        logs.Add(vcs.NewLogItem(c.Hash.String(), c.Committer.When.UTC(), c.Message))
7,347✔
353
                }
7,347✔
354
                return nil
22,032✔
355
        })
356
        return logs, nil
9✔
357
}
358

359
// EnableAutoPush sets a flag allowing to turn on/off git auto push operations
360
func (g *gitImpl) EnableAutoPush(flag bool) {
6✔
361
        if g.autoPushEnabled == flag {
6✔
362
                return
×
363
        }
×
364
        g.autoPushEnabled = flag
6✔
365
        autoPushStr := "off"
6✔
366
        if g.autoPushEnabled {
9✔
367
                autoPushStr = "on"
3✔
368
        }
3✔
369
        report.PostInfo(fmt.Sprintf("Git auto-push is turned %v", autoPushStr))
6✔
370
}
371

372
// IsAutoPushEnabled indicates if git auto-push operations are turned on
373
func (g *gitImpl) IsAutoPushEnabled() bool {
9✔
374
        return g.autoPushEnabled
9✔
375
}
9✔
376

377
// CheckRemoteAccess returns true if git remote can be accessed. This is currently done through
378
// checking the return value of "git push --dry-run". This very likely does not guarantee that
379
// git remote commands will work, but already gives an indication.
380
func (g *gitImpl) CheckRemoteAccess() bool {
18✔
381
        if g.remoteName != "" && g.workingBranch != "" && g.IsRemoteEnabled() {
24✔
382
                _, err := g.runGit("push", "--dry-run", g.GetRemoteName(), g.GetWorkingBranch())
6✔
383
                return err == nil
6✔
384
        }
6✔
385
        return false
12✔
386
}
387

388
// SupportsEmojis indicates if the VCS supports emojis in commit messages (true in case of git)
389
func (*gitImpl) SupportsEmojis() bool {
3✔
390
        return true
3✔
391
}
3✔
392

393
// traceGit runs a git command and traces its output.
394
// The command is launched from the git root directory
395
func (g *gitImpl) traceGit(args ...string) error {
57✔
396
        return g.traceGitFunction(g.buildGitArgs(args...)...)
57✔
397
}
57✔
398

399
// runGit calls git command in a separate process and returns its output traces
400
// The command is launched from the git root directory
401
func (g *gitImpl) runGit(args ...string) (output []byte, err error) {
36✔
402
        return g.runGitFunction(g.buildGitArgs(args...)...)
36✔
403
}
36✔
404

405
func (g *gitImpl) buildGitArgs(args ...string) []string {
93✔
406
        return append([]string{"-C", g.GetRootDir()}, args...)
93✔
407
}
93✔
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