• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
Build has been canceled!

murex / TCR / 11290247502

11 Oct 2024 09:52AM UTC coverage: 90.441% (-0.3%) from 90.716%
11290247502

push

github

mengdaming
[#37] Update TCR help doc with new option -g/--git-remote

5147 of 5691 relevant lines covered (90.44%)

5594.29 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
        "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
// New initializes the git implementation based on the provided directory from local clone
62
func New(dir string, remoteName string) (vcs.Interface, error) {
6✔
63
        return newGitImpl(plainOpen, dir, remoteName)
6✔
64
}
6✔
65

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

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

84
        g.rootDir = retrieveRootDir(g.filesystem)
174✔
85

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

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

99
        return &g, err
174✔
100
}
101

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

344
        var cIter object.CommitIter
9✔
345
        cIter, err = repo.Log(&git.LogOptions{From: head.Hash()})
9✔
346
        if err != nil {
9✔
347
                return nil, err
×
348
        }
×
349
        _ = cIter.ForEach(func(c *object.Commit) error {
17,721✔
350
                if msgFilter == nil || msgFilter(c.Message) {
23,619✔
351
                        logs.Add(vcs.NewLogItem(c.Hash.String(), c.Committer.When.UTC(), c.Message))
5,907✔
352
                }
5,907✔
353
                return nil
17,712✔
354
        })
355
        return logs, nil
9✔
356
}
357

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

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

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

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

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

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