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

murex / TCR / 11125150518

01 Oct 2024 12:19PM UTC coverage: 90.716% (-0.06%) from 90.779%
11125150518

push

github

mengdaming
[#674] Add "invariant" to variant CLI parameter short description

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

87 existing lines in 5 files now uncovered.

5130 of 5655 relevant lines covered (90.72%)

5581.14 hits per line

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

89.82
/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
        "strconv"
40
        "strings"
41
)
42

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

46
// DefaultRemoteName is the alias used by default for the git remote repository
47
const DefaultRemoteName = "origin"
48

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

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

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

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

83
        g.rootDir = retrieveRootDir(g.filesystem)
165✔
84

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

90
        if isRemoteDefined(DefaultRemoteName, g.repository) {
168✔
91
                g.remoteEnabled = true
3✔
92
                g.remoteName = DefaultRemoteName
3✔
93
                g.workingBranchExistsOnRemote, err = g.isWorkingBranchOnRemote()
3✔
94
        }
3✔
95

96
        return &g, err
165✔
97
}
98

99
// Name returns VCS name
100
func (*gitImpl) Name() string {
6✔
101
        return Name
6✔
102
}
6✔
103

104
// SessionSummary provides a short description related to current VCS session summary
105
func (g *gitImpl) SessionSummary() string {
3✔
106
        return fmt.Sprintf("%s branch \"%s\"", g.Name(), g.GetWorkingBranch())
3✔
107
}
3✔
108

109
// plainOpen is the regular function used to open a repository
110
func plainOpen(dir string) (*git.Repository, billy.Filesystem, error) {
6✔
111
        repo, err := git.PlainOpenWithOptions(dir, &git.PlainOpenOptions{
6✔
112
                DetectDotGit:          true,
6✔
113
                EnableDotGitCommonDir: false,
6✔
114
        })
6✔
115
        if err != nil {
9✔
116
                return nil, nil, err
3✔
117
        }
3✔
118

119
        // Try to grab the repository Storer
120
        storage, ok := repo.Storer.(*filesystem.Storage)
3✔
121
        if !ok {
3✔
122
                return nil, nil, errors.New("repository storage is not filesystem.Storage")
×
123
        }
×
124
        return repo, storage.Filesystem(), nil
3✔
125
}
126

127
// isRemoteDefined returns true is the provided remote is defined in the repository
128
func isRemoteDefined(remoteName string, repo *git.Repository) bool {
165✔
129
        _, err := repo.Remote(remoteName)
165✔
130
        return err == nil
165✔
131
}
165✔
132

133
// isWorkingBranchOnRemote returns true is the working branch exists on remote repository
134
func (g *gitImpl) isWorkingBranchOnRemote() (onRemote bool, err error) {
3✔
135
        var branches storer.ReferenceIter
3✔
136
        branches, err = remoteBranches(g.repository.Storer)
3✔
137
        if err != nil {
3✔
138
                return false, err
×
139
        }
×
140

141
        remoteBranchName := fmt.Sprintf("%v/%v", g.GetRemoteName(), g.GetWorkingBranch())
3✔
142
        _ = branches.ForEach(func(branch *plumbing.Reference) error {
21✔
143
                onRemote = onRemote || strings.HasSuffix(branch.Name().Short(), remoteBranchName)
18✔
144
                return nil
18✔
145
        })
18✔
146
        return onRemote, err
3✔
147
}
148

149
// remoteBranches returns the list of known remote branches
150
func remoteBranches(s storer.ReferenceStorer) (storer.ReferenceIter, error) {
3✔
151
        refs, err := s.IterReferences()
3✔
152
        if err != nil {
3✔
153
                return nil, err
×
154
        }
×
155

156
        // We keep only remote branches, and ignore symbolic references
157
        return storer.NewReferenceFilteredIter(func(ref *plumbing.Reference) bool {
183✔
158
                return ref.Name().IsRemote() && ref.Type() != plumbing.SymbolicReference
180✔
159
        }, refs), nil
180✔
160
}
161

162
// retrieveRootDir returns the local clone's root directory of provided repository
163
func retrieveRootDir(fs billy.Filesystem) string {
165✔
164
        return filepath.Dir(fs.Root())
165✔
165
}
165✔
166

167
// retrieveWorkingBranch returns the current working branch for provided repository
168
func retrieveWorkingBranch(repository *git.Repository) (string, error) {
165✔
169
        // Repo with at least one commit
165✔
170
        head, err := repository.Head()
165✔
171
        if err == nil {
168✔
172
                return head.Name().Short(), nil
3✔
173
        }
3✔
174

175
        // Brand new repo: nothing is committed yet
176
        head, err = repository.Reference(plumbing.HEAD, false)
162✔
177
        if err != nil {
162✔
178
                return "", err
×
179
        }
×
180
        return head.Target().Short(), nil
162✔
181
}
182

183
// GetRootDir returns the root directory path
184
func (g *gitImpl) GetRootDir() string {
93✔
185
        return g.rootDir
93✔
186
}
93✔
187

188
// GetRemoteName returns the current git remote name
189
func (g *gitImpl) GetRemoteName() string {
45✔
190
        return g.remoteName
45✔
191
}
45✔
192

193
// IsRemoteEnabled indicates if git remote operations are enabled
194
func (g *gitImpl) IsRemoteEnabled() bool {
27✔
195
        return g.remoteEnabled
27✔
196
}
27✔
197

198
// GetWorkingBranch returns the current git working branch
199
func (g *gitImpl) GetWorkingBranch() string {
81✔
200
        return g.workingBranch
81✔
201
}
81✔
202

203
// IsOnRootBranch indicates if git is currently on its root branch or not.
204
// Current implementation is a trivial one, that returns true if the branch is called "main" or "master"
205
func (g *gitImpl) IsOnRootBranch() bool {
12✔
206
        for _, b := range []string{"main", "master"} {
33✔
207
                if b == g.GetWorkingBranch() {
27✔
208
                        return true
6✔
209
                }
6✔
210
        }
211
        return false
6✔
212
}
213

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

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

241
// nothingToCommit returns true if there is nothing to commit
242
func (g *gitImpl) nothingToCommit() bool {
18✔
243
        worktree, _ := g.repository.Worktree()
18✔
244
        status, err := worktree.Status()
18✔
245
        if err != nil {
18✔
UNCOV
246
                return false
×
UNCOV
247
        }
×
248
        return status.IsClean()
18✔
249
}
250

251
// RevertLocal restores to last commit for the provided path.
252
// Current implementation uses a direct call to git
253
func (g *gitImpl) RevertLocal(path string) error {
6✔
254
        report.PostWarning("Reverting ", path)
6✔
255
        return g.traceGit("checkout", "HEAD", "--", path)
6✔
256
}
6✔
257

258
// RollbackLastCommit runs a git revert operation.
259
// Current implementation uses a direct call to git
260
func (g *gitImpl) RollbackLastCommit() error {
6✔
261
        report.PostInfo("Reverting changes")
6✔
262
        return g.traceGit("revert", "--no-gpg-sign", "--no-edit", "--no-commit", "HEAD")
6✔
263
}
6✔
264

265
// Push runs a git push operation.
266
// Current implementation uses a direct call to git
267
func (g *gitImpl) Push() error {
12✔
268
        if !g.IsRemoteEnabled() {
12✔
UNCOV
269
                // There's nothing to do in this case
×
UNCOV
270
                return nil
×
UNCOV
271
        }
×
272

273
        report.PostInfo("Pushing changes to ", g.GetRemoteName(), "/", g.GetWorkingBranch())
12✔
274
        err := g.traceGit("push", "--no-recurse-submodules", g.GetRemoteName(), g.GetWorkingBranch())
12✔
275
        if err == nil {
18✔
276
                g.workingBranchExistsOnRemote = true
6✔
277
        }
6✔
278
        return err
12✔
279
}
280

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

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

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

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

334
        var cIter object.CommitIter
9✔
335
        cIter, err = repo.Log(&git.LogOptions{From: head.Hash()})
9✔
336
        if err != nil {
9✔
UNCOV
337
                return nil, err
×
UNCOV
338
        }
×
339
        _ = cIter.ForEach(func(c *object.Commit) error {
17,568✔
340
                if msgFilter == nil || msgFilter(c.Message) {
23,415✔
341
                        logs.Add(vcs.NewLogItem(c.Hash.String(), c.Committer.When.UTC(), c.Message))
5,856✔
342
                }
5,856✔
343
                return nil
17,559✔
344
        })
345
        return logs, nil
9✔
346
}
347

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

361
// IsAutoPushEnabled indicates if git auto-push operations are turned on
362
func (g *gitImpl) IsAutoPushEnabled() bool {
9✔
363
        return g.autoPushEnabled
9✔
364
}
9✔
365

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

377
// traceGit runs a git command and traces its output.
378
// The command is launched from the git root directory
379
func (g *gitImpl) traceGit(args ...string) error {
57✔
380
        return g.traceGitFunction(g.buildGitArgs(args...)...)
57✔
381
}
57✔
382

383
// runGit calls git command in a separate process and returns its output traces
384
// The command is launched from the git root directory
385
func (g *gitImpl) runGit(args ...string) (output []byte, err error) {
36✔
386
        return g.runGitFunction(g.buildGitArgs(args...)...)
36✔
387
}
36✔
388

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

© 2025 Coveralls, Inc