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

murex / TCR / 11290466677

11 Oct 2024 10:08AM UTC coverage: 90.54% (+0.01%) from 90.527%
11290466677

push

github

philou
[#805] Create GitImpl with remote in test (fix merge compilation isssue)

To support VCS that don't manage emojies

5178 of 5719 relevant lines covered (90.54%)

5954.15 hits per line

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

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

61
func (g *gitImpl) SupportsEmojis() bool {
3✔
62
        return true
3✔
63
}
3✔
64

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

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

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

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

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

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

103
        return &g, err
177✔
104
}
105

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

217
// IsOnRootBranch indicates if git is currently on its root branch or not.
218
// Current implementation is a trivial one, that returns true if the branch is called "main" or "master"
219
func (g *gitImpl) IsOnRootBranch() bool {
12✔
220
        for _, b := range []string{"main", "master"} {
33✔
221
                if b == g.GetWorkingBranch() {
27✔
222
                        return true
6✔
223
                }
6✔
224
        }
225
        return false
6✔
226
}
227

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

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

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

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

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

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

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

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

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

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

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

348
        var cIter object.CommitIter
9✔
349
        cIter, err = repo.Log(&git.LogOptions{From: head.Hash()})
9✔
350
        if err != nil {
9✔
351
                return nil, err
×
352
        }
×
353
        _ = cIter.ForEach(func(c *object.Commit) error {
17,865✔
354
                if msgFilter == nil || msgFilter(c.Message) {
23,811✔
355
                        logs.Add(vcs.NewLogItem(c.Hash.String(), c.Committer.When.UTC(), c.Message))
5,955✔
356
                }
5,955✔
357
                return nil
17,856✔
358
        })
359
        return logs, nil
9✔
360
}
361

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

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

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

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

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

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