Skip to content

Commit dc36149

Browse files
feat(alpha update): add --git-config flag with clear defaults and replacement behavior
Assisted-by: ChatGPT (OpenAI)
1 parent 00cd9b2 commit dc36149

File tree

4 files changed

+129
-36
lines changed

4 files changed

+129
-36
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
- `--preserve-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 `--preserve-path`. |
144-
| `--preserve-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+
| `--preserve-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`). |
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`. **Important**: if you pass this flag, your values **replace** the defaults. To keep them *and* add more, re-specify the defaults explicitly. |
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: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,24 @@ type Update struct {
7777
// CLI (`gh`) to be installed and authenticated in the local environment.
7878
OpenGhIssue bool
7979

80+
// GitConfig contains per-invocation Git configurations that are applied
81+
// to every `git` command via `git -c key=value`.
82+
//
83+
// Examples:
84+
// []string{"merge.renameLimit=999999"} // enable robust rename detection
85+
// []string{"diff.renameLimit=999999"} // improve diff rename detection
86+
// []string{"merge.conflictStyle=diff3"} // show ancestor in conflict markers
87+
// []string{"rerere.enabled=true"} // enable reuse recorded resolutions
88+
//
89+
// Defaults:
90+
// If not set by the user, it defaults to:
91+
// []string{"merge.renameLimit=999999", "diff.renameLimit=999999"}
92+
//
93+
// Behavior:
94+
// If the user passes --git-config flags, those values REPLACE the defaults.
95+
// To keep defaults and add more, explicitly re-specify them alongside extras.
96+
GitConfig []string
97+
8098
// Temporary branches created during the update process. These are internal to the run
8199
// and are surfaced for transparency/debugging:
82100
// - AncestorBranch: clean scaffold generated from FromVersion
@@ -160,7 +178,7 @@ Resolve conflicts there, complete the merge locally, and push the branch.
160178
// This helps apply new scaffolding changes while preserving custom code.
161179
func (opts *Update) Update() error {
162180
log.Info("Checking out base branch", "branch", opts.FromBranch)
163-
checkoutCmd := exec.Command("git", "checkout", opts.FromBranch)
181+
checkoutCmd := helpers.GitCmd(opts.GitConfig, "checkout", opts.FromBranch)
164182
if err := checkoutCmd.Run(); err != nil {
165183
return fmt.Errorf("failed to checkout base branch %s: %w", opts.FromBranch, err)
166184
}
@@ -219,7 +237,7 @@ func (opts *Update) Update() error {
219237
if opts.ShowCommits {
220238
log.Info("Keeping commits history")
221239
out := opts.getOutputBranchName()
222-
if err := exec.Command("git", "checkout", "-b", out, opts.MergeBranch).Run(); err != nil {
240+
if err := helpers.GitCmd(opts.GitConfig, "checkout", "-b", out, opts.MergeBranch).Run(); err != nil {
223241
return fmt.Errorf("checkout %s: %w", out, err)
224242
}
225243
} else {
@@ -233,8 +251,8 @@ func (opts *Update) Update() error {
233251
if opts.Push {
234252
if opts.Push {
235253
out := opts.getOutputBranchName()
236-
_ = exec.Command("git", "checkout", out).Run()
237-
if err := exec.Command("git", "push", "-u", "origin", out).Run(); err != nil {
254+
_ = helpers.GitCmd(opts.GitConfig, "checkout", out).Run()
255+
if err := helpers.GitCmd(opts.GitConfig, "push", "-u", "origin", out).Run(); err != nil {
238256
return fmt.Errorf("failed to push %s: %w", out, err)
239257
}
240258
}
@@ -309,7 +327,7 @@ func (opts *Update) openGitHubIssue(hasConflicts bool) error {
309327
}
310328

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

314332
branches := []string{
315333
opts.AncestorBranch,
@@ -324,8 +342,8 @@ func (opts *Update) cleanupTempBranches() {
324342
continue
325343
}
326344
// 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()
345+
if err := helpers.GitCmd(opts.GitConfig, "show-ref", "--verify", "--quiet", "refs/heads/"+b).Run(); err == nil {
346+
_ = helpers.GitCmd(opts.GitConfig, "branch", "-D", b).Run()
329347
}
330348
}
331349
}
@@ -345,7 +363,7 @@ func (opts *Update) preservePaths() {
345363
if p == "" {
346364
continue
347365
}
348-
if err := exec.Command("git", "checkout", opts.FromBranch, "--", p).Run(); err != nil {
366+
if err := helpers.GitCmd(opts.GitConfig, "checkout", opts.FromBranch, "--", p).Run(); err != nil {
349367
log.Warn("failed to restore preserved path", "path", p, "branch", opts.FromBranch, "error", err)
350368
}
351369
}
@@ -358,26 +376,26 @@ func (opts *Update) squashToOutputBranch(hasConflicts bool) error {
358376
out := opts.getOutputBranchName()
359377

360378
// 1) base -> out
361-
if err := exec.Command("git", "checkout", opts.FromBranch).Run(); err != nil {
379+
if err := helpers.GitCmd(opts.GitConfig, "checkout", opts.FromBranch).Run(); err != nil {
362380
return fmt.Errorf("checkout %s: %w", opts.FromBranch, err)
363381
}
364-
if err := exec.Command("git", "checkout", "-B", out, opts.FromBranch).Run(); err != nil {
382+
if err := helpers.GitCmd(opts.GitConfig, "checkout", "-B", out, opts.FromBranch).Run(); err != nil {
365383
return fmt.Errorf("create/reset %s from %s: %w", out, opts.FromBranch, err)
366384
}
367385

368386
// 2) clean worktree, then copy merge tree
369387
if err := helpers.CleanWorktree("output branch"); err != nil {
370388
return fmt.Errorf("output branch: %w", err)
371389
}
372-
if err := exec.Command("git", "checkout", opts.MergeBranch, "--", ".").Run(); err != nil {
390+
if err := helpers.GitCmd(opts.GitConfig, "checkout", opts.MergeBranch, "--", ".").Run(); err != nil {
373391
return fmt.Errorf("checkout %s content: %w", "merge", err)
374392
}
375393

376394
// 3) optionally restore preserved paths from base (tests assert on 'git restore …')
377395
opts.preservePaths()
378396

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

@@ -404,7 +422,7 @@ func regenerateProjectWithVersion(version string) error {
404422
// prepareAncestorBranch prepares the ancestor branch by checking it out,
405423
// cleaning up the project files, and regenerating the project with the specified version.
406424
func (opts *Update) prepareAncestorBranch() error {
407-
if err := exec.Command("git", "checkout", "-b", opts.AncestorBranch, opts.FromBranch).Run(); err != nil {
425+
if err := helpers.GitCmd(opts.GitConfig, "checkout", "-b", opts.AncestorBranch, opts.FromBranch).Run(); err != nil {
408426
return fmt.Errorf("failed to create %s from %s: %w", opts.AncestorBranch, opts.FromBranch, err)
409427
}
410428
if err := cleanupBranch(); err != nil {
@@ -413,7 +431,7 @@ func (opts *Update) prepareAncestorBranch() error {
413431
if err := regenerateProjectWithVersion(opts.FromVersion); err != nil {
414432
return fmt.Errorf("failed to regenerate project with fromVersion %s: %w", opts.FromVersion, err)
415433
}
416-
gitCmd := exec.Command("git", "add", "--all")
434+
gitCmd := helpers.GitCmd(opts.GitConfig, "add", "--all")
417435
if err := gitCmd.Run(); err != nil {
418436
return fmt.Errorf("failed to stage changes in %s: %w", opts.AncestorBranch, err)
419437
}
@@ -488,17 +506,17 @@ func envWithPrefixedPath(dir string) []string {
488506
// populates it with the user's actual project content from the default branch.
489507
// This represents the current state of the user's project.
490508
func (opts *Update) prepareOriginalBranch() error {
491-
gitCmd := exec.Command("git", "checkout", "-b", opts.OriginalBranch)
509+
gitCmd := helpers.GitCmd(opts.GitConfig, "checkout", "-b", opts.OriginalBranch)
492510
if err := gitCmd.Run(); err != nil {
493511
return fmt.Errorf("failed to checkout branch %s: %w", opts.OriginalBranch, err)
494512
}
495513

496-
gitCmd = exec.Command("git", "checkout", opts.FromBranch, "--", ".")
514+
gitCmd = helpers.GitCmd(opts.GitConfig, "checkout", opts.FromBranch, "--", ".")
497515
if err := gitCmd.Run(); err != nil {
498516
return fmt.Errorf("failed to checkout content from %s branch onto %s: %w", opts.FromBranch, opts.OriginalBranch, err)
499517
}
500518

501-
gitCmd = exec.Command("git", "add", "--all")
519+
gitCmd = helpers.GitCmd(opts.GitConfig, "add", "--all")
502520
if err := gitCmd.Run(); err != nil {
503521
return fmt.Errorf("failed to stage all changes in current: %w", err)
504522
}
@@ -515,13 +533,13 @@ func (opts *Update) prepareOriginalBranch() error {
515533
// generates fresh scaffolding using the current (latest) CLI version.
516534
// This represents what the project should look like with the new version.
517535
func (opts *Update) prepareUpgradeBranch() error {
518-
gitCmd := exec.Command("git", "checkout", "-b", opts.UpgradeBranch, opts.AncestorBranch)
536+
gitCmd := helpers.GitCmd(opts.GitConfig, "checkout", "-b", opts.UpgradeBranch, opts.AncestorBranch)
519537
if err := gitCmd.Run(); err != nil {
520538
return fmt.Errorf("failed to checkout %s branch off %s: %w",
521539
opts.UpgradeBranch, opts.AncestorBranch, err)
522540
}
523541

524-
checkoutCmd := exec.Command("git", "checkout", opts.UpgradeBranch)
542+
checkoutCmd := helpers.GitCmd(opts.GitConfig, "checkout", opts.UpgradeBranch)
525543
if err := checkoutCmd.Run(); err != nil {
526544
return fmt.Errorf("failed to checkout base branch %s: %w", opts.UpgradeBranch, err)
527545
}
@@ -532,7 +550,7 @@ func (opts *Update) prepareUpgradeBranch() error {
532550
if err := regenerateProjectWithVersion(opts.ToVersion); err != nil {
533551
return fmt.Errorf("failed to regenerate project with version %s: %w", opts.ToVersion, err)
534552
}
535-
gitCmd = exec.Command("git", "add", "--all")
553+
gitCmd = helpers.GitCmd(opts.GitConfig, "add", "--all")
536554
if err := gitCmd.Run(); err != nil {
537555
return fmt.Errorf("failed to stage changes in %s: %w", opts.UpgradeBranch, err)
538556
}
@@ -546,17 +564,17 @@ func (opts *Update) prepareUpgradeBranch() error {
546564
// mergeOriginalToUpgrade attempts to merge the upgrade branch
547565
func (opts *Update) mergeOriginalToUpgrade() (bool, error) {
548566
hasConflicts := false
549-
if err := exec.Command("git", "checkout", "-b", opts.MergeBranch, opts.UpgradeBranch).Run(); err != nil {
567+
if err := helpers.GitCmd(opts.GitConfig, "checkout", "-b", opts.MergeBranch, opts.UpgradeBranch).Run(); err != nil {
550568
return hasConflicts, fmt.Errorf("failed to create merge branch %s from %s: %w",
551569
opts.MergeBranch, opts.UpgradeBranch, err)
552570
}
553571

554-
checkoutCmd := exec.Command("git", "checkout", opts.MergeBranch)
572+
checkoutCmd := helpers.GitCmd(opts.GitConfig, "checkout", opts.MergeBranch)
555573
if err := checkoutCmd.Run(); err != nil {
556574
return hasConflicts, fmt.Errorf("failed to checkout base branch %s: %w", opts.MergeBranch, err)
557575
}
558576

559-
mergeCmd := exec.Command("git", "merge", "--no-edit", "--no-commit", opts.OriginalBranch)
577+
mergeCmd := helpers.GitCmd(opts.GitConfig, "merge", "--no-edit", "--no-commit", opts.OriginalBranch)
560578
err := mergeCmd.Run()
561579
if err != nil {
562580
var exitErr *exec.ExitError
@@ -584,7 +602,7 @@ func (opts *Update) mergeOriginalToUpgrade() (bool, error) {
584602
runMakeTargets()
585603

586604
// Step 4: Stage and commit
587-
if err := exec.Command("git", "add", "--all").Run(); err != nil {
605+
if err := helpers.GitCmd(opts.GitConfig, "add", "--all").Run(); err != nil {
588606
return hasConflicts, fmt.Errorf("failed to stage merge results: %w", err)
589607
}
590608

0 commit comments

Comments
 (0)