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

kubernetes-sigs / kubebuilder / 16522004449

25 Jul 2025 12:30PM UTC coverage: 64.151%. Remained the same
16522004449

Pull #4898

github

web-flow
Merge branch 'master' into version/refactor-add-tests
Pull Request #4898: ✨ Improve version command output: add runtime fallbacks and unit tests.

2627 of 4095 relevant lines covered (64.15%)

13.59 hits per line

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

0.0
/pkg/cli/alpha/internal/update.go
1
/*
2
Copyright 2025 The Kubernetes Authors.
3

4
Licensed under the Apache License, Version 2.0 (the "License");
5
you may not use this file except in compliance with the License.
6
You may obtain a copy of the License at
7

8
    http://www.apache.org/licenses/LICENSE-2.0
9

10
Unless required by applicable law or agreed to in writing, software
11
distributed under the License is distributed on an "AS IS" BASIS,
12
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
See the License for the specific language governing permissions and
14
limitations under the License.
15
*/
16

17
package internal
18

19
import (
20
        "fmt"
21
        "io"
22
        "net/http"
23
        "os"
24
        "os/exec"
25
        "runtime"
26
        "strings"
27

28
        log "github.com/sirupsen/logrus"
29
        "github.com/spf13/afero"
30
        "golang.org/x/mod/semver"
31

32
        "sigs.k8s.io/kubebuilder/v4/pkg/config/store"
33
        "sigs.k8s.io/kubebuilder/v4/pkg/config/store/yaml"
34
        "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
35
        "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
36
)
37

38
// Update contains configuration for the update operation
39
type Update struct {
40
        // FromVersion specifies which version of Kubebuilder to use for the update.
41
        // If empty, the version from the PROJECT file will be used.
42
        FromVersion string
43
        // FromBranch specifies which branch to use as current when updating
44
        FromBranch string
45
        // CliVersion holds the version to be used during the upgrade process
46
        CliVersion string
47
        // BinaryURL holds the URL for downloading the specified binary from
48
        // the releases on GitHub
49
        BinaryURL string
50
}
51

52
// Update performs a complete project update by creating a three-way merge to help users
53
// upgrade their Kubebuilder projects. The process creates multiple Git branches:
54
// - ancestor: Clean state with old Kubebuilder version scaffolding
55
// - current: User's current project state
56
// - upgrade: New Kubebuilder version scaffolding
57
// - merge: Attempts to merge upgrade changes into current state
58
func (opts *Update) Update() error {
×
59
        // Download the specific Kubebuilder binary version for generating clean scaffolding
×
60
        tempDir, err := opts.downloadKubebuilderBinary()
×
61
        if err != nil {
×
62
                return fmt.Errorf("failed to download Kubebuilder %s binary: %w", opts.CliVersion, err)
×
63
        }
×
64
        log.Infof("Downloaded binary kept at %s for debugging purposes", tempDir)
×
65

×
66
        // Create ancestor branch with clean state for three-way merge
×
67
        if err := opts.checkoutAncestorBranch(); err != nil {
×
68
                return fmt.Errorf("failed to checkout the ancestor branch: %w", err)
×
69
        }
×
70

71
        // Remove all existing files to create a clean slate for re-scaffolding
72
        if err := opts.cleanUpAncestorBranch(); err != nil {
×
73
                return fmt.Errorf("failed to clean up the ancestor branch: %w", err)
×
74
        }
×
75

76
        // Generate clean scaffolding using the old Kubebuilder version
77
        if err := opts.runAlphaGenerate(tempDir, opts.CliVersion); err != nil {
×
78
                return fmt.Errorf("failed to run alpha generate on ancestor branch: %w", err)
×
79
        }
×
80

81
        // Create current branch representing user's existing project state
82
        if err := opts.checkoutCurrentOffAncestor(); err != nil {
×
83
                return fmt.Errorf("failed to checkout current off ancestor: %w", err)
×
84
        }
×
85

86
        // Create upgrade branch with new Kubebuilder version scaffolding
87
        if err := opts.checkoutUpgradeOffAncestor(); err != nil {
×
88
                return fmt.Errorf("failed to checkout upgrade off ancestor: %w", err)
×
89
        }
×
90

91
        // Create merge branch to attempt automatic merging of changes
92
        if err := opts.checkoutMergeOffCurrent(); err != nil {
×
93
                return fmt.Errorf("failed to checkout merge branch off current: %w", err)
×
94
        }
×
95

96
        // Attempt to merge upgrade changes into the user's current state
97
        if err := opts.mergeUpgradeIntoMerge(); err != nil {
×
98
                return fmt.Errorf("failed to merge upgrade into merge branch: %w", err)
×
99
        }
×
100

101
        return nil
×
102
}
103

104
// downloadKubebuilderBinary downloads the specified version of Kubebuilder binary
105
// from GitHub releases and saves it to a temporary directory with executable permissions.
106
// Returns the temporary directory path containing the binary.
107
func (opts *Update) downloadKubebuilderBinary() (string, error) {
×
108
        // Construct GitHub release URL based on current OS and architecture
×
109
        url := opts.BinaryURL
×
110

×
111
        log.Infof("Downloading the Kubebuilder %s binary from: %s", opts.CliVersion, url)
×
112

×
113
        // Create temporary directory for storing the downloaded binary
×
114
        fs := afero.NewOsFs()
×
115
        tempDir, err := afero.TempDir(fs, "", "kubebuilder"+opts.CliVersion+"-")
×
116
        if err != nil {
×
117
                return "", fmt.Errorf("failed to create temporary directory: %w", err)
×
118
        }
×
119

120
        // Create the binary file in the temporary directory
121
        binaryPath := tempDir + "/kubebuilder"
×
122
        file, err := os.Create(binaryPath)
×
123
        if err != nil {
×
124
                return "", fmt.Errorf("failed to create the binary file: %w", err)
×
125
        }
×
126
        defer func() {
×
127
                if err = file.Close(); err != nil {
×
128
                        log.Errorf("failed to close the file: %v", err)
×
129
                }
×
130
        }()
131

132
        // Download the binary from GitHub releases
133
        response, err := http.Get(url)
×
134
        if err != nil {
×
135
                return "", fmt.Errorf("failed to download the binary: %w", err)
×
136
        }
×
137
        defer func() {
×
138
                if err = response.Body.Close(); err != nil {
×
139
                        log.Errorf("failed to close the connection: %v", err)
×
140
                }
×
141
        }()
142

143
        // Check if download was successful
144
        if response.StatusCode != http.StatusOK {
×
145
                return "", fmt.Errorf("failed to download the binary: HTTP %d", response.StatusCode)
×
146
        }
×
147

148
        // Copy the downloaded content to the local file
149
        _, err = io.Copy(file, response.Body)
×
150
        if err != nil {
×
151
                return "", fmt.Errorf("failed to write the binary content to file: %w", err)
×
152
        }
×
153

154
        // Make the binary executable
155
        if err := os.Chmod(binaryPath, 0o755); err != nil {
×
156
                return "", fmt.Errorf("failed to make binary executable: %w", err)
×
157
        }
×
158

159
        log.Infof("Kubebuilder version %s successfully downloaded to %s", opts.CliVersion, binaryPath)
×
160

×
161
        return tempDir, nil
×
162
}
163

164
// checkoutAncestorBranch creates and switches to the 'ancestor' branch.
165
// This branch will serve as the common ancestor for the three-way merge,
166
// containing clean scaffolding from the old Kubebuilder version.
167
func (opts *Update) checkoutAncestorBranch() error {
×
168
        gitCmd := exec.Command("git", "checkout", "-b", "tmp-kb-update-ancestor")
×
169
        if err := gitCmd.Run(); err != nil {
×
170
                return fmt.Errorf("failed to create and checkout ancestor branch: %w", err)
×
171
        }
×
172
        log.Info("Created and checked out ancestor branch")
×
173

×
174
        return nil
×
175
}
176

177
// cleanUpAncestorBranch removes all files from the ancestor branch to create
178
// a clean state for re-scaffolding. This ensures the ancestor branch only
179
// contains pure scaffolding without any user modifications.
180
func (opts *Update) cleanUpAncestorBranch() error {
×
181
        log.Info("Cleaning all files and folders except .git and PROJECT")
×
182
        // Remove all tracked files from the Git repository
×
183
        cmd := exec.Command("find", ".", "-mindepth", "1", "-maxdepth", "1",
×
184
                "!", "-name", ".git",
×
185
                "!", "-name", "PROJECT",
×
186
                "-exec", "rm", "-rf", "{}", "+")
×
187
        log.Infof("Running cleanup command: %v", cmd.Args)
×
188
        if err := cmd.Run(); err != nil {
×
189
                return fmt.Errorf("failed to clean up files in ancestor branch: %w", err)
×
190
        }
×
191
        log.Info("Successfully cleanup files in ancestor branch")
×
192

×
193
        // Remove all untracked files and directories
×
194
        gitCmd := exec.Command("git", "add", ".")
×
195
        if err := gitCmd.Run(); err != nil {
×
196
                return fmt.Errorf("failed to stage changes in ancestor: %w", err)
×
197
        }
×
198
        log.Info("Successfully staged changes in ancestor")
×
199

×
200
        // Commit the cleanup to establish the clean state
×
201
        gitCmd = exec.Command("git", "commit", "-m", "Clean up the ancestor branch")
×
202
        if err := gitCmd.Run(); err != nil {
×
203
                return fmt.Errorf("failed to commit the cleanup in ancestor branch: %w", err)
×
204
        }
×
205
        log.Info("Successfully committed cleanup on ancestor")
×
206

×
207
        return nil
×
208
}
209

210
// runMakeTargets is a helper function to run make with the targets necessary
211
// to ensure all the necessary components are generated, formatted and linted.
212
func runMakeTargets() error {
×
213
        targets := []string{"manifests", "generate", "fmt", "vet", "lint-fix"}
×
214
        for _, target := range targets {
×
215
                log.Infof("Running: make %s", target)
×
216
                err := util.RunCmd(fmt.Sprintf("Running make %s", target), "make", target)
×
217
                if err != nil {
×
218
                        return fmt.Errorf("make %s failed: %v", target, err)
×
219
                }
×
220
        }
221
        return nil
×
222
}
223

224
// runAlphaGenerate executes the old Kubebuilder version's 'alpha generate' command
225
// to create clean scaffolding in the ancestor branch. This uses the downloaded
226
// binary with the original PROJECT file to recreate the project's initial state.
227
func (opts *Update) runAlphaGenerate(tempDir, version string) error {
×
228
        // Temporarily modify PATH to use the downloaded Kubebuilder binary
×
229
        tempBinaryPath := tempDir + "/kubebuilder"
×
230
        originalPath := os.Getenv("PATH")
×
231
        tempEnvPath := tempDir + ":" + originalPath
×
232

×
233
        if err := os.Setenv("PATH", tempEnvPath); err != nil {
×
234
                return fmt.Errorf("failed to set temporary PATH: %w", err)
×
235
        }
×
236

237
        // Restore original PATH when function completes
238
        defer func() {
×
239
                if err := os.Setenv("PATH", originalPath); err != nil {
×
240
                        log.Errorf("failed to restore original PATH: %v", err)
×
241
                }
×
242
        }()
243

244
        // Prepare the alpha generate command with proper I/O redirection
245
        cmd := exec.Command(tempBinaryPath, "alpha", "generate")
×
246
        cmd.Stdout = os.Stdout
×
247
        cmd.Stderr = os.Stderr
×
248
        cmd.Env = os.Environ()
×
249

×
250
        // Execute the alpha generate command to create clean scaffolding
×
251
        if err := cmd.Run(); err != nil {
×
252
                return fmt.Errorf("failed to run alpha generate: %w", err)
×
253
        }
×
254
        log.Info("Successfully ran alpha generate using Kubebuilder ", version)
×
255

×
256
        // Run make targets to ensure all the necessary components are generated,
×
257
        // formatted and linted.
×
258
        log.Info("Running 'make manifests generate fmt vet lint-fix'")
×
259
        if err := runMakeTargets(); err != nil {
×
260
                return fmt.Errorf("failed to run make: %w", err)
×
261
        }
×
262
        log.Info("Successfully ran make targets in ancestor")
×
263

×
264
        // Stage all generated files
×
265
        gitCmd := exec.Command("git", "add", ".")
×
266
        if err := gitCmd.Run(); err != nil {
×
267
                return fmt.Errorf("failed to stage changes in ancestor: %w", err)
×
268
        }
×
269
        log.Info("Successfully staged all changes in ancestor")
×
270

×
271
        // Commit the re-scaffolded project to the ancestor branch
×
272
        gitCmd = exec.Command("git", "commit", "-m", "Re-scaffold in ancestor")
×
273
        if err := gitCmd.Run(); err != nil {
×
274
                return fmt.Errorf("failed to commit changes in ancestor: %w", err)
×
275
        }
×
276
        log.Info("Successfully committed changes in ancestor")
×
277

×
278
        return nil
×
279
}
280

281
// checkoutCurrentOffAncestor creates the 'current' branch from ancestor and
282
// populates it with the user's actual project content from the default branch.
283
// This represents the current state of the user's project.
284
func (opts *Update) checkoutCurrentOffAncestor() error {
×
285
        // Create current branch starting from the clean ancestor state
×
286
        gitCmd := exec.Command("git", "checkout", "-b", "tmp-kb-update-current", "tmp-kb-update-ancestor")
×
287
        if err := gitCmd.Run(); err != nil {
×
288
                return fmt.Errorf("failed to checkout current branch off ancestor: %w", err)
×
289
        }
×
290
        log.Info("Successfully checked out current branch off ancestor")
×
291

×
292
        // Overlay the user's actual project content from default branch
×
293
        gitCmd = exec.Command("git", "checkout", opts.FromBranch, "--", ".")
×
294
        if err := gitCmd.Run(); err != nil {
×
295
                return fmt.Errorf("failed to checkout content from default branch onto current: %w", err)
×
296
        }
×
297
        log.Info("Successfully checked out content from main onto current branch")
×
298

×
299
        // Stage all the user's current project content
×
300
        gitCmd = exec.Command("git", "add", ".")
×
301
        if err := gitCmd.Run(); err != nil {
×
302
                return fmt.Errorf("failed to stage all changes in current: %w", err)
×
303
        }
×
304
        log.Info("Successfully staged all changes in current")
×
305

×
306
        // Commit the user's current state to the current branch
×
307
        gitCmd = exec.Command("git", "commit", "-m", "Add content from main onto current branch")
×
308
        if err := gitCmd.Run(); err != nil {
×
309
                return fmt.Errorf("failed to commit changes: %w", err)
×
310
        }
×
311
        log.Info("Successfully committed changes in current")
×
312

×
313
        return nil
×
314
}
315

316
// checkoutUpgradeOffAncestor creates the 'upgrade' branch from ancestor and
317
// generates fresh scaffolding using the current (latest) Kubebuilder version.
318
// This represents what the project should look like with the new version.
319
func (opts *Update) checkoutUpgradeOffAncestor() error {
×
320
        // Create upgrade branch starting from the clean ancestor state
×
321
        gitCmd := exec.Command("git", "checkout", "-b", "tmp-kb-update-upgrade", "tmp-kb-update-ancestor")
×
322
        if err := gitCmd.Run(); err != nil {
×
323
                return fmt.Errorf("failed to checkout upgrade branch off ancestor: %w", err)
×
324
        }
×
325
        log.Info("Successfully checked out upgrade branch off ancestor")
×
326

×
327
        // Run alpha generate with the current (new) Kubebuilder version
×
328
        // This uses the system's installed kubebuilder binary
×
329
        cmd := exec.Command("kubebuilder", "alpha", "generate")
×
330
        cmd.Stdout = os.Stdout
×
331
        cmd.Stderr = os.Stderr
×
332

×
333
        if err := cmd.Run(); err != nil {
×
334
                return fmt.Errorf("failed to run alpha generate on upgrade branch: %w", err)
×
335
        }
×
336
        log.Info("Successfully ran alpha generate on upgrade branch")
×
337

×
338
        // Run make targets to ensure all the necessary components are generated,
×
339
        // formatted and linted.
×
340
        log.Info("Running 'make manifests generate fmt vet lint-fix'")
×
341
        if err := runMakeTargets(); err != nil {
×
342
                return fmt.Errorf("failed to run make: %w", err)
×
343
        }
×
344
        log.Info("Successfully ran make targets in upgrade")
×
345

×
346
        // Stage all the newly generated files
×
347
        gitCmd = exec.Command("git", "add", ".")
×
348
        if err := gitCmd.Run(); err != nil {
×
349
                return fmt.Errorf("failed to stage changes on upgrade: %w", err)
×
350
        }
×
351
        log.Info("Successfully staged all changes in upgrade branch")
×
352

×
353
        // Commit the new version's scaffolding to the upgrade branch
×
354
        gitCmd = exec.Command("git", "commit", "-m", "alpha generate in upgrade branch")
×
355
        if err := gitCmd.Run(); err != nil {
×
356
                return fmt.Errorf("failed to commit changes in upgrade branch: %w", err)
×
357
        }
×
358
        log.Info("Successfully committed changes in upgrade branch")
×
359

×
360
        return nil
×
361
}
362

363
// checkoutMergeOffCurrent creates the 'merge' branch from the current branch.
364
// This branch will be used to attempt automatic merging of upgrade changes
365
// with the user's current project state.
366
func (opts *Update) checkoutMergeOffCurrent() error {
×
367
        gitCmd := exec.Command("git", "checkout", "-b", "tmp-kb-update-merge", "tmp-kb-update-current")
×
368
        if err := gitCmd.Run(); err != nil {
×
369
                return fmt.Errorf("failed to checkout merge branch off current: %w", err)
×
370
        }
×
371

372
        return nil
×
373
}
374

375
// mergeUpgradeIntoMerge attempts to merge the upgrade branch (containing new
376
// Kubebuilder scaffolding) into the merge branch (containing user's current state).
377
// If conflicts occur, it warns the user to resolve them manually rather than failing.
378
func (opts *Update) mergeUpgradeIntoMerge() error {
×
379
        gitCmd := exec.Command("git", "merge", "upgrade")
×
380
        err := gitCmd.Run()
×
381
        if err != nil {
×
382
                // Check if the error is due to merge conflicts (exit code 1)
×
383
                if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
×
384
                        log.Warn("Merge with conflicts. Please resolve them manually")
×
385
                        return nil // Don't treat conflicts as fatal errors
×
386
                }
×
387
                return fmt.Errorf("failed to merge the upgrade branch into the merge branch: %w", err)
×
388
        }
389

390
        // Run make targets to ensure all the necessary components are generated,
391
        // formatted and linted.
392
        log.Info("Running 'make manifests generate fmt vet lint-fix'")
×
393
        if err := runMakeTargets(); err != nil {
×
394
                return fmt.Errorf("failed to run make: %w", err)
×
395
        }
×
396
        log.Info("Successfully ran make targets in merge")
×
397

×
398
        return nil
×
399
}
400

401
// Validate checks if the user is in a git repository and if the repository is in a clean state.
402
// It also validates if the version specified by the user is in a valid format and available for
403
// download as a binary.
404
func (opts *Update) Validate() error {
×
405
        // Validate git repository
×
406
        if err := opts.validateGitRepo(); err != nil {
×
407
                return fmt.Errorf("failed to validate git repository: %w", err)
×
408
        }
×
409

410
        // Validate --from-branch
411
        if err := opts.validateFromBranch(); err != nil {
×
412
                return fmt.Errorf("failed to validate --from-branch: %w", err)
×
413
        }
×
414

415
        // Load the PROJECT configuration file
416
        projectConfigFile, err := opts.loadConfigFile()
×
417
        if err != nil {
×
418
                return fmt.Errorf("failed to load the PROJECT file: %w", err)
×
419
        }
×
420

421
        // Extract the cliVersion field from the PROJECT file
422
        opts.CliVersion = projectConfigFile.Config().GetCliVersion()
×
423

×
424
        // Determine which Kubebuilder version to use for the update
×
425
        if err := opts.defineFromVersion(); err != nil {
×
426
                return fmt.Errorf("failed to define version: %w", err)
×
427
        }
×
428

429
        // Validate if the specified version is available as a binary in the releases
430
        if err := opts.validateBinaryAvailability(); err != nil {
×
431
                return fmt.Errorf("failed to validate binary availability: %w", err)
×
432
        }
×
433

434
        return nil
×
435
}
436

437
// Load the PROJECT configuration file to get the current CLI version
438
func (opts *Update) loadConfigFile() (store.Store, error) {
×
439
        projectConfigFile := yaml.New(machinery.Filesystem{FS: afero.NewOsFs()})
×
440
        // TODO: assess if DefaultPath could be renamed to a more self-descriptive name
×
441
        if err := projectConfigFile.LoadFrom(yaml.DefaultPath); err != nil {
×
442
                if _, statErr := os.Stat(yaml.DefaultPath); os.IsNotExist(statErr) {
×
443
                        return projectConfigFile, fmt.Errorf("no PROJECT file found. Make sure you're in the project root directory")
×
444
                }
×
445
                return projectConfigFile, fmt.Errorf("fail to load the PROJECT file: %w", err)
×
446
        }
447
        return projectConfigFile, nil
×
448
}
449

450
// Define the version of the binary to be downloaded
451
func (opts *Update) defineFromVersion() error {
×
452
        // Allow override of the version from PROJECT file via command line flag
×
453
        if opts.FromVersion != "" {
×
454
                if !semver.IsValid(opts.FromVersion) {
×
455
                        return fmt.Errorf("invalid semantic version. Expect: vX.Y.Z (Ex: v4.5.0)")
×
456
                }
×
457
                opts.CliVersion = opts.FromVersion
×
458
        }
459

460
        if opts.CliVersion == "" {
×
461
                return fmt.Errorf("failed to retrieve Kubebuilder version from PROJECT file. Please use --from-version to inform it")
×
462
        }
×
463

464
        return nil
×
465
}
466

467
// Validate if the version specified is available as a binary for download
468
// from the releases
469
func (opts *Update) validateBinaryAvailability() error {
×
470
        // Ensure version has 'v' prefix for consistency with GitHub releases
×
471
        if !strings.HasPrefix(opts.CliVersion, "v") {
×
472
                opts.CliVersion = "v" + opts.CliVersion
×
473
        }
×
474

475
        // Construct the URL for pulling the binary from GitHub releases
476
        opts.BinaryURL = fmt.Sprintf("https://github.com/kubernetes-sigs/kubebuilder/releases/download/%s/kubebuilder_%s_%s",
×
477
                opts.CliVersion, runtime.GOOS, runtime.GOARCH)
×
478

×
479
        resp, err := http.Head(opts.BinaryURL)
×
480
        if err != nil {
×
481
                return fmt.Errorf("failed to check binary availability: %w", err)
×
482
        }
×
483
        defer func() {
×
484
                if err = resp.Body.Close(); err != nil {
×
485
                        log.Errorf("failed to close connection: %s", err)
×
486
                }
×
487
        }()
488

489
        switch resp.StatusCode {
×
490
        case http.StatusOK:
×
491
                log.Infof("Binary version %v is available", opts.CliVersion)
×
492
                return nil
×
493
        case http.StatusNotFound:
×
494
                return fmt.Errorf("binary version %s not found. Check versions available in releases",
×
495
                        opts.CliVersion)
×
496
        default:
×
497
                return fmt.Errorf("unexpected response %d when checking binary availability for version %s",
×
498
                        resp.StatusCode, opts.CliVersion)
×
499
        }
500
}
501

502
// Validate if in a git repository with clean state
503
func (opts *Update) validateGitRepo() error {
×
504
        // Check if in a git repository
×
505
        gitCmd := exec.Command("git", "rev-parse", "--git-dir")
×
506
        if err := gitCmd.Run(); err != nil {
×
507
                return fmt.Errorf("not in a git repository")
×
508
        }
×
509

510
        // Check if the branch has uncommitted changes
511
        gitCmd = exec.Command("git", "status", "--porcelain")
×
512
        output, err := gitCmd.Output()
×
513
        if err != nil {
×
514
                return fmt.Errorf("failed to check branch status: %w", err)
×
515
        }
×
516

517
        if len(strings.TrimSpace(string(output))) > 0 {
×
518
                return fmt.Errorf("working directory has uncommitted changes. Please commit or stash them before updating")
×
519
        }
×
520

521
        return nil
×
522
}
523

524
// Validate the branch passed to the --from-branch flag
525
func (opts *Update) validateFromBranch() error {
×
526
        // Set default if not specified
×
527
        if opts.FromBranch == "" {
×
528
                opts.FromBranch = "main"
×
529
        }
×
530

531
        // Check if the branch exists
532
        gitCmd := exec.Command("git", "rev-parse", "--verify", opts.FromBranch)
×
533
        if err := gitCmd.Run(); err != nil {
×
534
                return fmt.Errorf("%s branch does not exist locally. Run 'git branch -a' to see all available branches",
×
535
                        opts.FromBranch)
×
536
        }
×
537

538
        return nil
×
539
}
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