Skip to content

Commit 1b89844

Browse files
feat(alpha update): add --git-config flag with clear defaults and replacement behavior
Assisted-by: ChatGPT (OpenAI) Co-authored-by: Vitor Floriano <[email protected]>
1 parent 6d293e7 commit 1b89844

File tree

4 files changed

+131
-38
lines changed

4 files changed

+131
-38
lines changed

docs/book/src/reference/commands/alpha_update.md

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ The command creates three temporary branches:
5353
- `--restore-path`: in squash mode, restore specific files (like CI configs) from your base branch.
5454
- `--output-branch`: pick a custom branch name.
5555
- `--push`: push the result to `origin` automatically.
56+
- `--git-config`: sets git configurations.
5657

5758
### Step 5: Cleanup
5859
- Once the output branch is ready, all the temporary working branches are deleted.
@@ -132,19 +133,46 @@ make manifests generate fmt vet lint-fix
132133
make all
133134
```
134135

136+
### Changing Extra Git configs only during the run (does not change your ~/.gitconfig)_
137+
138+
By default, `kubebuilder alpha update` applies safe Git configs:
139+
`merge.renameLimit=999999`, `diff.renameLimit=999999`.
140+
You can add more, or disable them.
141+
142+
- **Add more on top of defaults**
143+
```shell
144+
kubebuilder alpha update \
145+
--git-config merge.conflictStyle=diff3 \
146+
--git-config rerere.enabled=true
147+
```
148+
149+
- **Disable defaults entirely**
150+
```shell
151+
kubebuilder alpha update --git-config disable
152+
```
153+
154+
- **Disable defaults and set your own**
155+
156+
```shell
157+
kubebuilder alpha update \
158+
--git-config disable \
159+
--git-config rerere.enabled=true
160+
```
161+
135162
## Flags
136163

137-
| Flag | Description |
138-
|-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
139-
| `--from-version` | Kubebuilder release to update **from** (e.g., `v4.6.0`). If unset, read from the `PROJECT` file when possible. |
140-
| `--to-version` | Kubebuilder release to update **to** (e.g., `v4.7.0`). If unset, defaults to the latest available release. |
141-
| `--from-branch` | Git branch that holds your current project code. Defaults to `main`. |
142-
| `--force` | Continue even if merge conflicts happen. Conflicted files are committed with conflict markers (CI/cron friendly). |
143-
| `--show-commits` | Keep full history (do not squash). **Not compatible** with `--restore-path`. |
144-
| `--restore-path` | Repeatable. **Squash mode only.** After copying the merge tree to the output branch, restore these paths from the base branch (e.g., `.github/workflows`). |
145-
| `--output-branch` | Name of the output branch. Default: `kubebuilder-update-from-<from-version>-to-<to-version>`. |
146-
| `--push` | Push the output branch to the `origin` remote after the update completes. |
147-
| `-h, --help` | Show help for this command. |
164+
| Flag | Description |
165+
|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
166+
| `--from-version` | Kubebuilder release to update **from** (e.g., `v4.6.0`). If unset, read from the `PROJECT` file when possible. |
167+
| `--to-version` | Kubebuilder release to update **to** (e.g., `v4.7.0`). If unset, defaults to the latest available release. |
168+
| `--from-branch` | Git branch that holds your current project code. Defaults to `main`. |
169+
| `--force` | Continue even if merge conflicts happen. Conflicted files are committed with conflict markers (CI/cron friendly). |
170+
| `--show-commits` | Keep full history (do not squash). **Not compatible** with `--preserve-path`. |
171+
| `--restore-path` | Repeatable. Paths to preserve from the base branch (repeatable). Not supported with --show-commits. (e.g., `.github/workflows`). |
172+
| `--output-branch` | Name of the output branch. Default: `kubebuilder-update-from-<from-version>-to-<to-version>`. |
173+
| `--push` | Push the output branch to the `origin` remote after the update completes. |
174+
| `--git-config` | Repeatable. Pass per-invocation Git config as `-c key=value`. **Default** (if omitted): `-c merge.renameLimit=999999 -c diff.renameLimit=999999`. Your configs are applied on top. To disable defaults, include `--git-config disable` |
175+
| `-h, --help` | Show help for this command. |
148176

149177
<aside class="note warning">
150178
<h1>You might need to upgrade your project first</h1>

pkg/cli/alpha/internal/update/helpers/git_commands.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,13 @@ func CleanWorktree(label string) error {
4747
}
4848
return nil
4949
}
50+
51+
// GitCmd creates a new git command with the provided git configuration
52+
func GitCmd(gitConfig []string, args ...string) *exec.Cmd {
53+
gitArgs := make([]string, 0, len(gitConfig)*2+len(args))
54+
for _, kv := range gitConfig {
55+
gitArgs = append(gitArgs, "-c", kv)
56+
}
57+
gitArgs = append(gitArgs, args...)
58+
return exec.Command("git", gitArgs...)
59+
}

pkg/cli/alpha/internal/update/update.go

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,25 @@ type Update struct {
7777
// CLI (`gh`) to be installed and authenticated in the local environment.
7878
OpenGhIssue bool
7979

80+
// GitConfig holds per-invocation Git settings applied to every `git` command via
81+
// `git -c key=value`.
82+
//
83+
// Examples:
84+
// []string{"merge.renameLimit=999999"} // improve rename detection during merges
85+
// []string{"diff.renameLimit=999999"} // improve rename detection during diffs
86+
// []string{"merge.conflictStyle=diff3"} // show ancestor in conflict markers
87+
// []string{"rerere.enabled=true"} // reuse recorded resolutions
88+
//
89+
// Defaults:
90+
// When no --git-config flags are provided, the updater adds:
91+
// []string{"merge.renameLimit=999999", "diff.renameLimit=999999"}
92+
//
93+
// Behavior:
94+
// • If one or more --git-config flags are supplied, those values are appended on top of the defaults.
95+
// • To disable the defaults entirely, include a literal "disable", for example:
96+
// --git-config disable --git-config rerere.enabled=true
97+
GitConfig []string
98+
8099
// Temporary branches created during the update process. These are internal to the run
81100
// and are surfaced for transparency/debugging:
82101
// - AncestorBranch: clean scaffold generated from FromVersion
@@ -160,7 +179,7 @@ Resolve conflicts there, complete the merge locally, and push the branch.
160179
// This helps apply new scaffolding changes while preserving custom code.
161180
func (opts *Update) Update() error {
162181
log.Info("Checking out base branch", "branch", opts.FromBranch)
163-
checkoutCmd := exec.Command("git", "checkout", opts.FromBranch)
182+
checkoutCmd := helpers.GitCmd(opts.GitConfig, "checkout", opts.FromBranch)
164183
if err := checkoutCmd.Run(); err != nil {
165184
return fmt.Errorf("failed to checkout base branch %s: %w", opts.FromBranch, err)
166185
}
@@ -219,7 +238,7 @@ func (opts *Update) Update() error {
219238
if opts.ShowCommits {
220239
log.Info("Keeping commits history")
221240
out := opts.getOutputBranchName()
222-
if err := exec.Command("git", "checkout", "-b", out, opts.MergeBranch).Run(); err != nil {
241+
if err := helpers.GitCmd(opts.GitConfig, "checkout", "-b", out, opts.MergeBranch).Run(); err != nil {
223242
return fmt.Errorf("checkout %s: %w", out, err)
224243
}
225244
} else {
@@ -233,8 +252,8 @@ func (opts *Update) Update() error {
233252
if opts.Push {
234253
if opts.Push {
235254
out := opts.getOutputBranchName()
236-
_ = exec.Command("git", "checkout", out).Run()
237-
if err := exec.Command("git", "push", "-u", "origin", out).Run(); err != nil {
255+
_ = helpers.GitCmd(opts.GitConfig, "checkout", out).Run()
256+
if err := helpers.GitCmd(opts.GitConfig, "push", "-u", "origin", out).Run(); err != nil {
238257
return fmt.Errorf("failed to push %s: %w", out, err)
239258
}
240259
}
@@ -309,7 +328,7 @@ func (opts *Update) openGitHubIssue(hasConflicts bool) error {
309328
}
310329

311330
func (opts *Update) cleanupTempBranches() {
312-
_ = exec.Command("git", "checkout", opts.getOutputBranchName()).Run()
331+
_ = helpers.GitCmd(opts.GitConfig, "checkout", opts.getOutputBranchName()).Run()
313332

314333
branches := []string{
315334
opts.AncestorBranch,
@@ -324,8 +343,8 @@ func (opts *Update) cleanupTempBranches() {
324343
continue
325344
}
326345
// Delete only if it's a LOCAL branch.
327-
if err := exec.Command("git", "show-ref", "--verify", "--quiet", "refs/heads/"+b).Run(); err == nil {
328-
_ = exec.Command("git", "branch", "-D", b).Run()
346+
if err := helpers.GitCmd(opts.GitConfig, "show-ref", "--verify", "--quiet", "refs/heads/"+b).Run(); err == nil {
347+
_ = helpers.GitCmd(opts.GitConfig, "branch", "-D", b).Run()
329348
}
330349
}
331350
}
@@ -345,7 +364,7 @@ func (opts *Update) preservePaths() {
345364
if p == "" {
346365
continue
347366
}
348-
if err := exec.Command("git", "checkout", opts.FromBranch, "--", p).Run(); err != nil {
367+
if err := helpers.GitCmd(opts.GitConfig, "checkout", opts.FromBranch, "--", p).Run(); err != nil {
349368
log.Warn("failed to restore preserved path", "path", p, "branch", opts.FromBranch, "error", err)
350369
}
351370
}
@@ -358,26 +377,26 @@ func (opts *Update) squashToOutputBranch(hasConflicts bool) error {
358377
out := opts.getOutputBranchName()
359378

360379
// 1) base -> out
361-
if err := exec.Command("git", "checkout", opts.FromBranch).Run(); err != nil {
380+
if err := helpers.GitCmd(opts.GitConfig, "checkout", opts.FromBranch).Run(); err != nil {
362381
return fmt.Errorf("checkout %s: %w", opts.FromBranch, err)
363382
}
364-
if err := exec.Command("git", "checkout", "-B", out, opts.FromBranch).Run(); err != nil {
383+
if err := helpers.GitCmd(opts.GitConfig, "checkout", "-B", out, opts.FromBranch).Run(); err != nil {
365384
return fmt.Errorf("create/reset %s from %s: %w", out, opts.FromBranch, err)
366385
}
367386

368387
// 2) clean worktree, then copy merge tree
369388
if err := helpers.CleanWorktree("output branch"); err != nil {
370389
return fmt.Errorf("output branch: %w", err)
371390
}
372-
if err := exec.Command("git", "checkout", opts.MergeBranch, "--", ".").Run(); err != nil {
391+
if err := helpers.GitCmd(opts.GitConfig, "checkout", opts.MergeBranch, "--", ".").Run(); err != nil {
373392
return fmt.Errorf("checkout %s content: %w", "merge", err)
374393
}
375394

376395
// 3) optionally restore preserved paths from base (tests assert on 'git restore …')
377396
opts.preservePaths()
378397

379398
// 4) stage and single squashed commit
380-
if err := exec.Command("git", "add", "--all").Run(); err != nil {
399+
if err := helpers.GitCmd(opts.GitConfig, "add", "--all").Run(); err != nil {
381400
return fmt.Errorf("stage output: %w", err)
382401
}
383402

@@ -404,7 +423,7 @@ func regenerateProjectWithVersion(version string) error {
404423
// prepareAncestorBranch prepares the ancestor branch by checking it out,
405424
// cleaning up the project files, and regenerating the project with the specified version.
406425
func (opts *Update) prepareAncestorBranch() error {
407-
if err := exec.Command("git", "checkout", "-b", opts.AncestorBranch, opts.FromBranch).Run(); err != nil {
426+
if err := helpers.GitCmd(opts.GitConfig, "checkout", "-b", opts.AncestorBranch, opts.FromBranch).Run(); err != nil {
408427
return fmt.Errorf("failed to create %s from %s: %w", opts.AncestorBranch, opts.FromBranch, err)
409428
}
410429
if err := cleanupBranch(); err != nil {
@@ -413,7 +432,7 @@ func (opts *Update) prepareAncestorBranch() error {
413432
if err := regenerateProjectWithVersion(opts.FromVersion); err != nil {
414433
return fmt.Errorf("failed to regenerate project with fromVersion %s: %w", opts.FromVersion, err)
415434
}
416-
gitCmd := exec.Command("git", "add", "--all")
435+
gitCmd := helpers.GitCmd(opts.GitConfig, "add", "--all")
417436
if err := gitCmd.Run(); err != nil {
418437
return fmt.Errorf("failed to stage changes in %s: %w", opts.AncestorBranch, err)
419438
}
@@ -508,17 +527,17 @@ func envWithPrefixedPath(dir string) []string {
508527
// populates it with the user's actual project content from the default branch.
509528
// This represents the current state of the user's project.
510529
func (opts *Update) prepareOriginalBranch() error {
511-
gitCmd := exec.Command("git", "checkout", "-b", opts.OriginalBranch)
530+
gitCmd := helpers.GitCmd(opts.GitConfig, "checkout", "-b", opts.OriginalBranch)
512531
if err := gitCmd.Run(); err != nil {
513532
return fmt.Errorf("failed to checkout branch %s: %w", opts.OriginalBranch, err)
514533
}
515534

516-
gitCmd = exec.Command("git", "checkout", opts.FromBranch, "--", ".")
535+
gitCmd = helpers.GitCmd(opts.GitConfig, "checkout", opts.FromBranch, "--", ".")
517536
if err := gitCmd.Run(); err != nil {
518537
return fmt.Errorf("failed to checkout content from %s branch onto %s: %w", opts.FromBranch, opts.OriginalBranch, err)
519538
}
520539

521-
gitCmd = exec.Command("git", "add", "--all")
540+
gitCmd = helpers.GitCmd(opts.GitConfig, "add", "--all")
522541
if err := gitCmd.Run(); err != nil {
523542
return fmt.Errorf("failed to stage all changes in current: %w", err)
524543
}
@@ -535,13 +554,13 @@ func (opts *Update) prepareOriginalBranch() error {
535554
// generates fresh scaffolding using the current (latest) CLI version.
536555
// This represents what the project should look like with the new version.
537556
func (opts *Update) prepareUpgradeBranch() error {
538-
gitCmd := exec.Command("git", "checkout", "-b", opts.UpgradeBranch, opts.AncestorBranch)
557+
gitCmd := helpers.GitCmd(opts.GitConfig, "checkout", "-b", opts.UpgradeBranch, opts.AncestorBranch)
539558
if err := gitCmd.Run(); err != nil {
540559
return fmt.Errorf("failed to checkout %s branch off %s: %w",
541560
opts.UpgradeBranch, opts.AncestorBranch, err)
542561
}
543562

544-
checkoutCmd := exec.Command("git", "checkout", opts.UpgradeBranch)
563+
checkoutCmd := helpers.GitCmd(opts.GitConfig, "checkout", opts.UpgradeBranch)
545564
if err := checkoutCmd.Run(); err != nil {
546565
return fmt.Errorf("failed to checkout base branch %s: %w", opts.UpgradeBranch, err)
547566
}
@@ -552,7 +571,7 @@ func (opts *Update) prepareUpgradeBranch() error {
552571
if err := regenerateProjectWithVersion(opts.ToVersion); err != nil {
553572
return fmt.Errorf("failed to regenerate project with version %s: %w", opts.ToVersion, err)
554573
}
555-
gitCmd = exec.Command("git", "add", "--all")
574+
gitCmd = helpers.GitCmd(opts.GitConfig, "add", "--all")
556575
if err := gitCmd.Run(); err != nil {
557576
return fmt.Errorf("failed to stage changes in %s: %w", opts.UpgradeBranch, err)
558577
}
@@ -566,17 +585,17 @@ func (opts *Update) prepareUpgradeBranch() error {
566585
// mergeOriginalToUpgrade attempts to merge the upgrade branch
567586
func (opts *Update) mergeOriginalToUpgrade() (bool, error) {
568587
hasConflicts := false
569-
if err := exec.Command("git", "checkout", "-b", opts.MergeBranch, opts.UpgradeBranch).Run(); err != nil {
588+
if err := helpers.GitCmd(opts.GitConfig, "checkout", "-b", opts.MergeBranch, opts.UpgradeBranch).Run(); err != nil {
570589
return hasConflicts, fmt.Errorf("failed to create merge branch %s from %s: %w",
571590
opts.MergeBranch, opts.UpgradeBranch, err)
572591
}
573592

574-
checkoutCmd := exec.Command("git", "checkout", opts.MergeBranch)
593+
checkoutCmd := helpers.GitCmd(opts.GitConfig, "checkout", opts.MergeBranch)
575594
if err := checkoutCmd.Run(); err != nil {
576595
return hasConflicts, fmt.Errorf("failed to checkout base branch %s: %w", opts.MergeBranch, err)
577596
}
578597

579-
mergeCmd := exec.Command("git", "merge", "--no-edit", "--no-commit", opts.OriginalBranch)
598+
mergeCmd := helpers.GitCmd(opts.GitConfig, "merge", "--no-edit", "--no-commit", opts.OriginalBranch)
580599
err := mergeCmd.Run()
581600
if err != nil {
582601
var exitErr *exec.ExitError
@@ -608,7 +627,7 @@ func (opts *Update) mergeOriginalToUpgrade() (bool, error) {
608627
}
609628

610629
// Step 4: Stage and commit
611-
if err := exec.Command("git", "add", "--all").Run(); err != nil {
630+
if err := helpers.GitCmd(opts.GitConfig, "add", "--all").Run(); err != nil {
612631
return hasConflicts, fmt.Errorf("failed to stage merge results: %w", err)
613632
}
614633

0 commit comments

Comments
 (0)