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

umputun / ralphex / 21488035598

29 Jan 2026 05:23PM UTC coverage: 79.809% (-0.1%) from 79.951%
21488035598

push

github

umputun
feat(prompts): add {{DEFAULT_BRANCH}} template variable

- detect default branch from origin/HEAD or common branch names
- replace hardcoded "master" in review prompts with {{DEFAULT_BRANCH}}
- expand template variables inside agent content
- verify local branch exists before using (fall back to origin/branch)
- document template variables in README.md, CLAUDE.md, llms.txt

Related to #45

61 of 97 new or added lines in 5 files covered. (62.89%)

4 existing lines in 2 files now uncovered.

3925 of 4918 relevant lines covered (79.81%)

61.41 hits per line

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

74.21
/pkg/git/git.go
1
// Package git provides git repository operations using go-git library.
2
package git
3

4
import (
5
        "errors"
6
        "fmt"
7
        "log"
8
        "os"
9
        "path/filepath"
10
        "sort"
11
        "strings"
12
        "time"
13

14
        "github.com/go-git/go-billy/v5/osfs"
15
        "github.com/go-git/go-git/v5"
16
        "github.com/go-git/go-git/v5/config"
17
        "github.com/go-git/go-git/v5/plumbing"
18
        "github.com/go-git/go-git/v5/plumbing/format/gitignore"
19
        "github.com/go-git/go-git/v5/plumbing/object"
20
)
21

22
// repo provides low-level git operations using go-git.
23
// This is an internal type - use Service for the public API.
24
type repo struct {
25
        gitRepo *git.Repository
26
        path    string // absolute path to repository root
27
}
28

29
// Root returns the absolute path to the repository root.
30
func (r *repo) Root() string {
4✔
31
        return r.path
4✔
32
}
4✔
33

34
// toRelative converts a path to be relative to the repository root.
35
// Absolute paths are converted to repo-relative.
36
// Relative paths starting with ".." are resolved against CWD first.
37
// Other relative paths are assumed to already be repo-relative.
38
// Returns error if the resolved path is outside the repository.
39
func (r *repo) toRelative(path string) (string, error) {
40✔
40
        // for relative paths, just clean and validate
40✔
41
        if !filepath.IsAbs(path) {
61✔
42
                cleaned := filepath.Clean(path)
21✔
43
                if strings.HasPrefix(cleaned, "..") {
23✔
44
                        return "", fmt.Errorf("path %q escapes repository root", path)
2✔
45
                }
2✔
46
                return cleaned, nil
19✔
47
        }
48

49
        // convert absolute path to repo-relative
50
        rel, err := filepath.Rel(r.path, path)
19✔
51
        if err != nil {
19✔
52
                return "", fmt.Errorf("path outside repository: %w", err)
×
53
        }
×
54

55
        if strings.HasPrefix(rel, "..") {
21✔
56
                return "", fmt.Errorf("path %q is outside repository root %q", path, r.path)
2✔
57
        }
2✔
58

59
        return rel, nil
17✔
60
}
61

62
// openRepo opens a git repository at the given path.
63
// Supports both regular repositories and git worktrees.
64
func openRepo(path string) (*repo, error) {
97✔
65
        gitRepo, err := git.PlainOpenWithOptions(path, &git.PlainOpenOptions{
97✔
66
                EnableDotGitCommonDir: true,
97✔
67
        })
97✔
68
        if err != nil {
99✔
69
                return nil, fmt.Errorf("open repository: %w", err)
2✔
70
        }
2✔
71

72
        // get the worktree root path
73
        wt, err := gitRepo.Worktree()
95✔
74
        if err != nil {
95✔
75
                return nil, fmt.Errorf("get worktree: %w", err)
×
76
        }
×
77

78
        return &repo{gitRepo: gitRepo, path: wt.Filesystem.Root()}, nil
95✔
79
}
80

81
// HasCommits returns true if the repository has at least one commit.
82
func (r *repo) HasCommits() (bool, error) {
9✔
83
        _, err := r.gitRepo.Head()
9✔
84
        if err != nil {
14✔
85
                if errors.Is(err, plumbing.ErrReferenceNotFound) {
10✔
86
                        return false, nil // no commits yet
5✔
87
                }
5✔
88
                return false, fmt.Errorf("get HEAD: %w", err)
×
89
        }
90
        return true, nil
4✔
91
}
92

93
// CreateInitialCommit stages all non-ignored files and creates an initial commit.
94
// Returns error if no files to stage or commit fails.
95
// Respects local, global, and system gitignore patterns via IsIgnored.
96
func (r *repo) CreateInitialCommit(message string) error {
7✔
97
        wt, err := r.gitRepo.Worktree()
7✔
98
        if err != nil {
7✔
99
                return fmt.Errorf("get worktree: %w", err)
×
100
        }
×
101

102
        // get status to find untracked files
103
        status, err := wt.Status()
7✔
104
        if err != nil {
7✔
105
                return fmt.Errorf("get status: %w", err)
×
106
        }
×
107

108
        // collect untracked paths and sort for deterministic staging order
109
        var paths []string
7✔
110
        for path, s := range status {
15✔
111
                if s.Worktree == git.Untracked {
16✔
112
                        paths = append(paths, path)
8✔
113
                }
8✔
114
        }
115
        sort.Strings(paths)
7✔
116

7✔
117
        // stage each untracked file that's not ignored
7✔
118
        staged := 0
7✔
119
        for _, path := range paths {
15✔
120
                ignored, ignoreErr := r.IsIgnored(path)
8✔
121
                if ignoreErr != nil {
8✔
122
                        return fmt.Errorf("check ignored %s: %w", path, ignoreErr)
×
123
                }
×
124
                if ignored {
9✔
125
                        continue
1✔
126
                }
127
                if _, addErr := wt.Add(path); addErr != nil {
7✔
128
                        return fmt.Errorf("stage %s: %w", path, addErr)
×
129
                }
×
130
                staged++
7✔
131
        }
132

133
        if staged == 0 {
9✔
134
                return errors.New("no files to commit")
2✔
135
        }
2✔
136

137
        author := r.getAuthor()
5✔
138
        _, err = wt.Commit(message, &git.CommitOptions{Author: author})
5✔
139
        if err != nil {
5✔
140
                return fmt.Errorf("commit: %w", err)
×
141
        }
×
142

143
        return nil
5✔
144
}
145

146
// CurrentBranch returns the name of the current branch, or empty string for detached HEAD state.
147
func (r *repo) CurrentBranch() (string, error) {
27✔
148
        head, err := r.gitRepo.Head()
27✔
149
        if err != nil {
27✔
150
                return "", fmt.Errorf("get HEAD: %w", err)
×
151
        }
×
152
        if !head.Name().IsBranch() {
29✔
153
                return "", nil // detached HEAD
2✔
154
        }
2✔
155
        return head.Name().Short(), nil
25✔
156
}
157

158
// CreateBranch creates a new branch and switches to it.
159
// Returns error if branch already exists to prevent data loss.
160
func (r *repo) CreateBranch(name string) error {
21✔
161
        wt, err := r.gitRepo.Worktree()
21✔
162
        if err != nil {
21✔
163
                return fmt.Errorf("get worktree: %w", err)
×
164
        }
×
165

166
        head, err := r.gitRepo.Head()
21✔
167
        if err != nil {
21✔
168
                return fmt.Errorf("get HEAD: %w", err)
×
169
        }
×
170

171
        branchRef := plumbing.NewBranchReferenceName(name)
21✔
172

21✔
173
        // check if branch already exists to prevent overwriting
21✔
174
        if _, err := r.gitRepo.Reference(branchRef, false); err == nil {
22✔
175
                return fmt.Errorf("branch %q already exists", name)
1✔
176
        }
1✔
177

178
        // create the branch reference pointing to current HEAD
179
        ref := plumbing.NewHashReference(branchRef, head.Hash())
20✔
180
        if err := r.gitRepo.Storer.SetReference(ref); err != nil {
20✔
181
                return fmt.Errorf("create branch reference: %w", err)
×
182
        }
×
183

184
        // create branch config for tracking
185
        branchConfig := &config.Branch{
20✔
186
                Name: name,
20✔
187
        }
20✔
188
        if err := r.gitRepo.CreateBranch(branchConfig); err != nil {
21✔
189
                // ignore if branch config already exists
1✔
190
                if !errors.Is(err, git.ErrBranchExists) {
2✔
191
                        return fmt.Errorf("create branch config: %w", err)
1✔
192
                }
1✔
193
        }
194

195
        // checkout the new branch, Keep preserves untracked files
196
        if err := wt.Checkout(&git.CheckoutOptions{Branch: branchRef, Keep: true}); err != nil {
19✔
197
                return fmt.Errorf("checkout branch: %w", err)
×
198
        }
×
199

200
        return nil
19✔
201
}
202

203
// BranchExists checks if a branch with the given name exists.
204
func (r *repo) BranchExists(name string) bool {
8✔
205
        branchRef := plumbing.NewBranchReferenceName(name)
8✔
206
        _, err := r.gitRepo.Reference(branchRef, false)
8✔
207
        return err == nil
8✔
208
}
8✔
209

210
// CheckoutBranch switches to an existing branch.
211
func (r *repo) CheckoutBranch(name string) error {
8✔
212
        wt, err := r.gitRepo.Worktree()
8✔
213
        if err != nil {
8✔
214
                return fmt.Errorf("get worktree: %w", err)
×
215
        }
×
216

217
        branchRef := plumbing.NewBranchReferenceName(name)
8✔
218
        if err := wt.Checkout(&git.CheckoutOptions{Branch: branchRef, Keep: true}); err != nil {
9✔
219
                return fmt.Errorf("checkout branch: %w", err)
1✔
220
        }
1✔
221
        return nil
7✔
222
}
223

224
// MoveFile moves a file using git (equivalent to git mv).
225
// Paths can be absolute or relative to the repository root.
226
// The destination directory must already exist.
227
func (r *repo) MoveFile(src, dst string) error {
7✔
228
        // convert to relative paths for git operations
7✔
229
        srcRel, err := r.toRelative(src)
7✔
230
        if err != nil {
8✔
231
                return fmt.Errorf("invalid source path: %w", err)
1✔
232
        }
1✔
233
        dstRel, err := r.toRelative(dst)
6✔
234
        if err != nil {
6✔
235
                return fmt.Errorf("invalid destination path: %w", err)
×
236
        }
×
237

238
        wt, err := r.gitRepo.Worktree()
6✔
239
        if err != nil {
6✔
240
                return fmt.Errorf("get worktree: %w", err)
×
241
        }
×
242

243
        srcAbs := filepath.Join(r.path, srcRel)
6✔
244
        dstAbs := filepath.Join(r.path, dstRel)
6✔
245

6✔
246
        // move the file on filesystem
6✔
247
        if err := os.Rename(srcAbs, dstAbs); err != nil {
7✔
248
                return fmt.Errorf("rename file: %w", err)
1✔
249
        }
1✔
250

251
        // stage the removal of old path
252
        if _, err := wt.Remove(srcRel); err != nil {
6✔
253
                // rollback filesystem change
1✔
254
                if rbErr := os.Rename(dstAbs, srcAbs); rbErr != nil {
1✔
255
                        log.Printf("[WARN] rollback failed after Remove error: %v", rbErr)
×
256
                }
×
257
                return fmt.Errorf("remove old path: %w", err)
1✔
258
        }
259

260
        // stage the addition of new path
261
        if _, err := wt.Add(dstRel); err != nil {
4✔
262
                // rollback: unstage removal and restore file
×
263
                if rbErr := os.Rename(dstAbs, srcAbs); rbErr != nil {
×
264
                        log.Printf("[WARN] rollback failed after Add error: %v", rbErr)
×
265
                }
×
266
                return fmt.Errorf("add new path: %w", err)
×
267
        }
268

269
        return nil
4✔
270
}
271

272
// Add stages a file for commit.
273
// Path can be absolute or relative to the repository root.
274
func (r *repo) Add(path string) error {
22✔
275
        rel, err := r.toRelative(path)
22✔
276
        if err != nil {
22✔
277
                return fmt.Errorf("invalid path: %w", err)
×
278
        }
×
279

280
        wt, err := r.gitRepo.Worktree()
22✔
281
        if err != nil {
22✔
282
                return fmt.Errorf("get worktree: %w", err)
×
283
        }
×
284

285
        if _, err := wt.Add(rel); err != nil {
23✔
286
                return fmt.Errorf("add file: %w", err)
1✔
287
        }
1✔
288

289
        return nil
21✔
290
}
291

292
// Commit creates a commit with the given message.
293
// Returns error if no changes are staged.
294
func (r *repo) Commit(msg string) error {
20✔
295
        wt, err := r.gitRepo.Worktree()
20✔
296
        if err != nil {
20✔
297
                return fmt.Errorf("get worktree: %w", err)
×
298
        }
×
299

300
        author := r.getAuthor()
20✔
301
        _, err = wt.Commit(msg, &git.CommitOptions{Author: author})
20✔
302
        if err != nil {
21✔
303
                return fmt.Errorf("commit: %w", err)
1✔
304
        }
1✔
305

306
        return nil
19✔
307
}
308

309
// getAuthor returns the commit author from git config or a fallback.
310
// checks repository config first (.git/config), then falls back to global config,
311
// and finally to default values.
312
func (r *repo) getAuthor() *object.Signature {
27✔
313
        // try repository config first (merges local + global)
27✔
314
        if cfg, err := r.gitRepo.Config(); err == nil {
54✔
315
                if cfg.User.Name != "" && cfg.User.Email != "" {
27✔
316
                        return &object.Signature{
×
317
                                Name:  cfg.User.Name,
×
318
                                Email: cfg.User.Email,
×
319
                                When:  time.Now(),
×
320
                        }
×
321
                }
×
322
        }
323

324
        // fallback to global config only
325
        if cfg, err := config.LoadConfig(config.GlobalScope); err == nil {
54✔
326
                if cfg.User.Name != "" && cfg.User.Email != "" {
27✔
327
                        return &object.Signature{
×
328
                                Name:  cfg.User.Name,
×
329
                                Email: cfg.User.Email,
×
330
                                When:  time.Now(),
×
331
                        }
×
332
                }
×
333
        }
334

335
        // fallback to default author
336
        return &object.Signature{
27✔
337
                Name:  "ralphex",
27✔
338
                Email: "ralphex@localhost",
27✔
339
                When:  time.Now(),
27✔
340
        }
27✔
341
}
342

343
// IsIgnored checks if a path is ignored by gitignore rules.
344
// Checks local .gitignore files, global gitignore (from core.excludesfile or default
345
// XDG location ~/.config/git/ignore), and system gitignore (/etc/gitconfig).
346
// Returns false, nil if no gitignore rules exist.
347
//
348
// Precedence (highest to lowest): local .gitignore > global > system.
349
// go-git's Matcher checks patterns from end-to-start, so patterns at end have higher priority.
350
func (r *repo) IsIgnored(path string) (bool, error) {
22✔
351
        wt, err := r.gitRepo.Worktree()
22✔
352
        if err != nil {
22✔
353
                return false, fmt.Errorf("get worktree: %w", err)
×
354
        }
×
355

356
        var patterns []gitignore.Pattern
22✔
357
        rootFS := osfs.New("/")
22✔
358

22✔
359
        // load system patterns first (lowest priority)
22✔
360
        if systemPatterns, err := gitignore.LoadSystemPatterns(rootFS); err == nil {
44✔
361
                patterns = append(patterns, systemPatterns...)
22✔
362
        }
22✔
363

364
        // load global patterns (middle priority)
365
        if globalPatterns, err := gitignore.LoadGlobalPatterns(rootFS); err == nil && len(globalPatterns) > 0 {
22✔
366
                patterns = append(patterns, globalPatterns...)
×
367
        } else {
22✔
368
                // fallback to default XDG location if core.excludesfile not set
22✔
369
                // git uses $XDG_CONFIG_HOME/git/ignore (defaults to ~/.config/git/ignore)
22✔
370
                patterns = append(patterns, loadXDGGlobalPatterns()...)
22✔
371
        }
22✔
372

373
        // load local patterns last (highest priority)
374
        localPatterns, _ := gitignore.ReadPatterns(wt.Filesystem, nil)
22✔
375
        patterns = append(patterns, localPatterns...)
22✔
376

22✔
377
        matcher := gitignore.NewMatcher(patterns)
22✔
378
        pathParts := strings.Split(filepath.ToSlash(path), "/")
22✔
379
        return matcher.Match(pathParts, false), nil
22✔
380
}
381

382
// loadXDGGlobalPatterns loads gitignore patterns from the default XDG location.
383
// Git checks $XDG_CONFIG_HOME/git/ignore, defaulting to ~/.config/git/ignore.
384
func loadXDGGlobalPatterns() []gitignore.Pattern {
22✔
385
        // check XDG_CONFIG_HOME first, fall back to ~/.config
22✔
386
        configHome := os.Getenv("XDG_CONFIG_HOME")
22✔
387
        if configHome == "" {
22✔
388
                home, err := os.UserHomeDir()
×
389
                if err != nil {
×
390
                        return nil
×
391
                }
×
392
                configHome = filepath.Join(home, ".config")
×
393
        }
394

395
        ignorePath := filepath.Join(configHome, "git", "ignore")
22✔
396
        data, err := os.ReadFile(ignorePath) //nolint:gosec // user's gitignore file
22✔
397
        if err != nil {
39✔
398
                return nil
17✔
399
        }
17✔
400

401
        var patterns []gitignore.Pattern
5✔
402
        for line := range strings.SplitSeq(string(data), "\n") {
15✔
403
                line = strings.TrimSpace(line)
10✔
404
                if line == "" || strings.HasPrefix(line, "#") {
15✔
405
                        continue
5✔
406
                }
407
                patterns = append(patterns, gitignore.ParsePattern(line, nil))
5✔
408
        }
409
        return patterns
5✔
410
}
411

412
// IsDirty returns true if the worktree has uncommitted changes
413
// (staged or modified tracked files).
414
func (r *repo) IsDirty() (bool, error) {
10✔
415
        wt, err := r.gitRepo.Worktree()
10✔
416
        if err != nil {
10✔
417
                return false, fmt.Errorf("get worktree: %w", err)
×
418
        }
×
419

420
        status, err := wt.Status()
10✔
421
        if err != nil {
10✔
422
                return false, fmt.Errorf("get status: %w", err)
×
423
        }
×
424

425
        for _, s := range status {
15✔
426
                // check for staged changes
5✔
427
                if s.Staging != git.Unmodified && s.Staging != git.Untracked {
6✔
428
                        return true, nil
1✔
429
                }
1✔
430
                // check for unstaged changes to tracked files
431
                if s.Worktree == git.Modified || s.Worktree == git.Deleted {
7✔
432
                        return true, nil
3✔
433
                }
3✔
434
        }
435

436
        return false, nil
6✔
437
}
438

439
// HasChangesOtherThan returns true if there are uncommitted changes to files other than the given file.
440
// this includes modified/deleted tracked files, staged changes, and untracked files (excluding gitignored).
441
func (r *repo) HasChangesOtherThan(filePath string) (bool, error) {
13✔
442
        wt, err := r.gitRepo.Worktree()
13✔
443
        if err != nil {
13✔
444
                return false, fmt.Errorf("get worktree: %w", err)
×
445
        }
×
446

447
        status, err := wt.Status()
13✔
448
        if err != nil {
13✔
449
                return false, fmt.Errorf("get status: %w", err)
×
450
        }
×
451

452
        relPath, err := r.normalizeToRelative(filePath)
13✔
453
        if err != nil {
13✔
454
                return false, err
×
455
        }
×
456

457
        for path, s := range status {
27✔
458
                if path == relPath {
23✔
459
                        continue // skip the target file
9✔
460
                }
461
                if !r.fileHasChanges(s) {
5✔
462
                        continue
×
463
                }
464
                // for untracked files, check if they're gitignored
465
                // note: go-git sets both Staging and Worktree to Untracked for untracked files
466
                if s.Worktree == git.Untracked {
9✔
467
                        ignored, err := r.IsIgnored(path)
4✔
468
                        if err != nil {
4✔
469
                                return false, fmt.Errorf("check ignored: %w", err)
×
470
                        }
×
471
                        if ignored {
4✔
472
                                continue // skip gitignored untracked files
×
473
                        }
474
                }
475
                return true, nil
5✔
476
        }
477

478
        return false, nil
8✔
479
}
480

481
// FileHasChanges returns true if the given file has uncommitted changes.
482
// this includes untracked, modified, deleted, or staged states.
483
func (r *repo) FileHasChanges(filePath string) (bool, error) {
11✔
484
        wt, err := r.gitRepo.Worktree()
11✔
485
        if err != nil {
11✔
486
                return false, fmt.Errorf("get worktree: %w", err)
×
487
        }
×
488

489
        status, err := wt.Status()
11✔
490
        if err != nil {
11✔
491
                return false, fmt.Errorf("get status: %w", err)
×
492
        }
×
493

494
        relPath, err := r.normalizeToRelative(filePath)
11✔
495
        if err != nil {
11✔
496
                return false, err
×
497
        }
×
498

499
        if s, ok := status[relPath]; ok {
18✔
500
                return r.fileHasChanges(s), nil
7✔
501
        }
7✔
502

503
        return false, nil
4✔
504
}
505

506
// normalizeToRelative converts a file path to be relative to the repository root.
507
func (r *repo) normalizeToRelative(filePath string) (string, error) {
24✔
508
        absPath, err := filepath.Abs(filePath)
24✔
509
        if err != nil {
24✔
510
                return "", fmt.Errorf("get absolute path: %w", err)
×
511
        }
×
512
        relPath, err := filepath.Rel(r.path, absPath)
24✔
513
        if err != nil {
24✔
514
                return "", fmt.Errorf("get relative path: %w", err)
×
515
        }
×
516
        return relPath, nil
24✔
517
}
518

519
// fileHasChanges checks if a file status indicates uncommitted changes.
520
func (r *repo) fileHasChanges(s *git.FileStatus) bool {
12✔
521
        return s.Staging != git.Unmodified ||
12✔
522
                s.Worktree == git.Modified || s.Worktree == git.Deleted || s.Worktree == git.Untracked
12✔
523
}
12✔
524

525
// IsMainBranch returns true if the current branch is "main" or "master".
526
func (r *repo) IsMainBranch() (bool, error) {
11✔
527
        branch, err := r.CurrentBranch()
11✔
528
        if err != nil {
11✔
529
                return false, fmt.Errorf("get current branch: %w", err)
×
530
        }
×
531
        return branch == "main" || branch == "master", nil
11✔
532
}
533

534
// GetDefaultBranch returns the default branch name.
535
// detects the default branch in this order:
536
// 1. check origin/HEAD symbolic reference (most reliable for repos with remotes)
537
// 2. check common branch names: main, master, trunk, develop
538
// 3. fall back to "master" if nothing else found
539
func (r *repo) GetDefaultBranch() string {
7✔
540
        // first, try to get the default branch from origin/HEAD
7✔
541
        if branch := r.getDefaultBranchFromOriginHead(); branch != "" {
8✔
542
                return branch
1✔
543
        }
1✔
544

545
        // fallback: check which common branch names exist
546
        branches, err := r.gitRepo.Branches()
6✔
547
        if err != nil {
6✔
NEW
548
                return "master"
×
NEW
549
        }
×
550

551
        branchSet := make(map[string]bool)
6✔
552
        _ = branches.ForEach(func(ref *plumbing.Reference) error {
14✔
553
                branchSet[ref.Name().Short()] = true
8✔
554
                return nil
8✔
555
        })
8✔
556

557
        // check common default branch names in order of preference
558
        for _, name := range []string{"main", "master", "trunk", "develop"} {
19✔
559
                if branchSet[name] {
18✔
560
                        return name
5✔
561
                }
5✔
562
        }
563

564
        return "master"
1✔
565
}
566

567
// getDefaultBranchFromOriginHead attempts to detect default branch from origin/HEAD symbolic ref.
568
// returns empty string if origin/HEAD doesn't exist or isn't a symbolic reference.
569
// returns local branch name if it exists, otherwise returns remote-tracking branch (e.g., "origin/main").
570
func (r *repo) getDefaultBranchFromOriginHead() string {
7✔
571
        ref, err := r.gitRepo.Reference(plumbing.NewRemoteReferenceName("origin", "HEAD"), false)
7✔
572
        if err != nil {
13✔
573
                return ""
6✔
574
        }
6✔
575
        if ref.Type() != plumbing.SymbolicReference {
1✔
NEW
576
                return ""
×
NEW
577
        }
×
578

579
        target := ref.Target().Short()
1✔
580
        if !strings.HasPrefix(target, "origin/") {
1✔
NEW
581
                return target
×
NEW
582
        }
×
583

584
        // target is like "origin/main", extract branch name
585
        branchName := target[7:]
1✔
586
        // verify local branch exists before returning it
1✔
587
        // if local doesn't exist, return remote-tracking ref which git commands understand
1✔
588
        localRef := plumbing.NewBranchReferenceName(branchName)
1✔
589
        if _, err := r.gitRepo.Reference(localRef, false); err == nil {
1✔
NEW
590
                return branchName
×
NEW
591
        }
×
592
        // local branch doesn't exist, use remote-tracking branch (e.g., "origin/main")
593
        return target
1✔
594
}
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