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

murex / TCR / 15156762416

21 May 2025 08:00AM UTC coverage: 90.473% (+0.04%) from 90.43%
15156762416

push

github

mengdaming
Update node dependencies

5242 of 5794 relevant lines covered (90.47%)

5610.16 hits per line

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

82.05
/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
        "github.com/go-git/go-billy/v5"
31
        "github.com/go-git/go-git/v5"
32
        "github.com/go-git/go-git/v5/plumbing"
33
        "github.com/go-git/go-git/v5/plumbing/object"
34
        "github.com/go-git/go-git/v5/plumbing/storer"
35
        "github.com/go-git/go-git/v5/storage/filesystem"
36
        "github.com/murex/tcr/report"
37
        "github.com/murex/tcr/vcs"
38
        "path/filepath"
39
        "slices"
40
        "strconv"
41
        "strings"
42
)
43

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

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

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

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

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

85
        g.rootDir = retrieveRootDir(g.filesystem)
177✔
86

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

92
        if isRemoteDefined(remoteName, g.repository) {
177✔
93
                report.PostInfo("Git remote name is ", g.remoteName)
×
94
                g.remoteEnabled = true
×
95
                g.workingBranchExistsOnRemote, err = g.isWorkingBranchOnRemote()
×
96
        } else {
177✔
97
                report.PostWarning("Git remote name not found: ", g.remoteName)
177✔
98
        }
177✔
99

100
        return &g, err
177✔
101
}
102

103
// Name returns VCS name
104
func (*gitImpl) Name() string {
15✔
105
        return Name
15✔
106
}
15✔
107

108
// SessionSummary provides a short description related to current VCS session summary
109
func (g *gitImpl) SessionSummary() string {
12✔
110
        var branch = g.GetWorkingBranch()
12✔
111
        if g.IsRemoteEnabled() {
18✔
112
                branch = fmt.Sprintf("%s/%s", g.GetRemoteName(), g.GetWorkingBranch())
6✔
113
        }
6✔
114
        return fmt.Sprintf("%s branch \"%s\"", g.Name(), branch)
12✔
115
}
116

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

127
        // Try to grab the repository Storer
128
        storage, ok := repo.Storer.(*filesystem.Storage)
3✔
129
        if !ok {
3✔
130
                return nil, nil, errors.New("repository storage is not filesystem.Storage")
×
131
        }
×
132
        return repo, storage.Filesystem(), nil
3✔
133
}
134

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

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

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

160
// remoteBranches returns the list of known remote branches
161
func remoteBranches(s storer.ReferenceStorer) (storer.ReferenceIter, error) {
×
162
        refs, err := s.IterReferences()
×
163
        if err != nil {
×
164
                return nil, err
×
165
        }
×
166

167
        // We keep only remote branches, and ignore symbolic references
168
        return storer.NewReferenceFilteredIter(func(ref *plumbing.Reference) bool {
×
169
                return ref.Name().IsRemote() && ref.Type() != plumbing.SymbolicReference
×
170
        }, refs), nil
×
171
}
172

173
// retrieveRootDir returns the local clone's root directory of provided repository
174
func retrieveRootDir(fs billy.Filesystem) string {
177✔
175
        return filepath.Dir(fs.Root())
177✔
176
}
177✔
177

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

186
        // Brand-new repo: nothing is committed yet
187
        head, err = repository.Reference(plumbing.HEAD, false)
174✔
188
        if err != nil {
174✔
189
                return "", err
×
190
        }
×
191
        return head.Target().Short(), nil
174✔
192
}
193

194
// GetRootDir returns the root directory path
195
func (g *gitImpl) GetRootDir() string {
93✔
196
        return g.rootDir
93✔
197
}
93✔
198

199
// GetRemoteName returns the current git remote name
200
func (g *gitImpl) GetRemoteName() string {
48✔
201
        return g.remoteName
48✔
202
}
48✔
203

204
// IsRemoteEnabled indicates if git remote operations are enabled
205
func (g *gitImpl) IsRemoteEnabled() bool {
39✔
206
        return g.remoteEnabled
39✔
207
}
39✔
208

209
// GetWorkingBranch returns the current git working branch
210
func (g *gitImpl) GetWorkingBranch() string {
84✔
211
        return g.workingBranch
84✔
212
}
84✔
213

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

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

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

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

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

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

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

279
        report.PostInfo("Pushing changes to ", g.GetRemoteName(), "/", g.GetWorkingBranch())
12✔
280
        err := g.traceGit("push", "--no-recurse-submodules", g.GetRemoteName(), g.GetWorkingBranch())
12✔
281
        if err == nil {
18✔
282
                g.workingBranchExistsOnRemote = true
6✔
283
        }
6✔
284
        return err
12✔
285
}
286

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

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

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

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

340
        var cIter object.CommitIter
9✔
341
        cIter, err = repo.Log(&git.LogOptions{From: head.Hash()})
9✔
342
        if err != nil {
9✔
343
                return nil, err
×
344
        }
×
345
        _ = cIter.ForEach(func(c *object.Commit) error {
20,079✔
346
                if msgFilter == nil || msgFilter(c.Message) {
26,763✔
347
                        logs.Add(vcs.NewLogItem(c.Hash.String(), c.Committer.When.UTC(), c.Message))
6,693✔
348
                }
6,693✔
349
                return nil
20,070✔
350
        })
351
        return logs, nil
9✔
352
}
353

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

367
// IsAutoPushEnabled indicates if git auto-push operations are turned on
368
func (g *gitImpl) IsAutoPushEnabled() bool {
9✔
369
        return g.autoPushEnabled
9✔
370
}
9✔
371

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

383
// SupportsEmojis indicates if the VCS supports emojis in commit messages (true in case of git)
384
func (*gitImpl) SupportsEmojis() bool {
3✔
385
        return true
3✔
386
}
3✔
387

388
// traceGit runs a git command and traces its output.
389
// The command is launched from the git root directory
390
func (g *gitImpl) traceGit(args ...string) error {
57✔
391
        return g.traceGitFunction(g.buildGitArgs(args...)...)
57✔
392
}
57✔
393

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

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