Skip to content

Commit

Permalink
Add command to rebase onto base branch (#3615)
Browse files Browse the repository at this point in the history
- **PR Description**

In the rebase menu, add a command "Rebase onto base branch". This makes
it more convenient to rebase onto master (or main), because
- you don't need to bring your local version of the base branch up to
date first
- you don't have to remember which of your main branches (e.g. "main",
"devel", or "1.0-hotfixes") your current branch is based on.

This is sitting on top of #3614.

Closes #3546.

- **Please check if the PR fulfills these requirements**

* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide)
* [x] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
* [x] Docs have been updated if necessary
* [x] You've read through your own file changes for silly mistakes etc
  • Loading branch information
stefanhaller committed Jun 3, 2024
2 parents 36a4696 + a8921a1 commit b856877
Show file tree
Hide file tree
Showing 17 changed files with 144 additions and 53 deletions.
30 changes: 9 additions & 21 deletions pkg/gui/controllers/branches_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,13 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty
DisplayOnScreen: true,
},
{
Key: opts.GetKey(opts.Config.Branches.RebaseBranch),
Handler: opts.Guards.OutsideFilterMode(self.rebase),
GetDisabledReason: self.require(
self.singleItemSelected(self.notRebasingOntoSelf),
),
Description: self.c.Tr.RebaseBranch,
Tooltip: self.c.Tr.RebaseBranchTooltip,
DisplayOnScreen: true,
Key: opts.GetKey(opts.Config.Branches.RebaseBranch),
Handler: opts.Guards.OutsideFilterMode(self.withItem(self.rebase)),
GetDisabledReason: self.require(self.singleItemSelected()),
Description: self.c.Tr.RebaseBranch,
Tooltip: self.c.Tr.RebaseBranchTooltip,
OpensMenu: true,
DisplayOnScreen: true,
},
{
Key: opts.GetKey(opts.Config.Branches.MergeIntoCurrentBranch),
Expand Down Expand Up @@ -633,19 +632,8 @@ func (self *BranchesController) merge() error {
return self.c.Helpers().MergeAndRebase.MergeRefIntoCheckedOutBranch(selectedBranchName)
}

func (self *BranchesController) rebase() error {
selectedBranchName := self.context().GetSelected().Name
return self.c.Helpers().MergeAndRebase.RebaseOntoRef(selectedBranchName)
}

func (self *BranchesController) notRebasingOntoSelf(branch *models.Branch) *types.DisabledReason {
selectedBranchName := branch.Name
checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef().Name
if selectedBranchName == checkedOutBranch {
return &types.DisabledReason{Text: self.c.Tr.CantRebaseOntoSelf}
}

return nil
func (self *BranchesController) rebase(branch *models.Branch) error {
return self.c.Helpers().MergeAndRebase.RebaseOntoRef(branch.Name)
}

func (self *BranchesController) fastForward(branch *models.Branch) error {
Expand Down
61 changes: 53 additions & 8 deletions pkg/gui/controllers/helpers/merge_and_rebase_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,11 +234,29 @@ func (self *MergeAndRebaseHelper) PromptToContinueRebase() error {
}

func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error {
checkedOutBranch := self.refsHelper.GetCheckedOutRef().Name
checkedOutBranch := self.refsHelper.GetCheckedOutRef()
checkedOutBranchName := self.refsHelper.GetCheckedOutRef().Name
var disabledReason, baseBranchDisabledReason *types.DisabledReason
if checkedOutBranchName == ref {
disabledReason = &types.DisabledReason{Text: self.c.Tr.CantRebaseOntoSelf}
}

baseBranch, err := self.c.Git().Loaders.BranchLoader.GetBaseBranch(checkedOutBranch, self.refsHelper.c.Model().MainBranches)
if err != nil {
return err
}
if baseBranch == "" {
baseBranch = self.c.Tr.CouldNotDetermineBaseBranch
baseBranchDisabledReason = &types.DisabledReason{Text: self.c.Tr.CouldNotDetermineBaseBranch}
}

menuItems := []*types.MenuItem{
{
Label: self.c.Tr.SimpleRebase,
Key: 's',
Label: utils.ResolvePlaceholderString(self.c.Tr.SimpleRebase,
map[string]string{"ref": ref},
),
Key: 's',
DisabledReason: disabledReason,
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(task gocui.Task) error {
Expand All @@ -258,9 +276,12 @@ func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error {
},
},
{
Label: self.c.Tr.InteractiveRebase,
Key: 'i',
Tooltip: self.c.Tr.InteractiveRebaseTooltip,
Label: utils.ResolvePlaceholderString(self.c.Tr.InteractiveRebase,
map[string]string{"ref": ref},
),
Key: 'i',
DisabledReason: disabledReason,
Tooltip: self.c.Tr.InteractiveRebaseTooltip,
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
baseCommit := self.c.Modes().MarkedBaseCommit.GetHash()
Expand All @@ -279,15 +300,39 @@ func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error {
return self.c.PushContext(self.c.Contexts().LocalCommits)
},
},
{
Label: utils.ResolvePlaceholderString(self.c.Tr.RebaseOntoBaseBranch,
map[string]string{"baseBranch": ShortBranchName(baseBranch)},
),
Key: 'b',
DisabledReason: baseBranchDisabledReason,
Tooltip: self.c.Tr.RebaseOntoBaseBranchTooltip,
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(task gocui.Task) error {
baseCommit := self.c.Modes().MarkedBaseCommit.GetHash()
var err error
if baseCommit != "" {
err = self.c.Git().Rebase.RebaseBranchFromBaseCommit(baseBranch, baseCommit)
} else {
err = self.c.Git().Rebase.RebaseBranch(baseBranch)
}
err = self.CheckMergeOrRebase(err)
if err == nil {
return self.ResetMarkedBaseCommit()
}
return err
})
},
},
}

title := utils.ResolvePlaceholderString(
lo.Ternary(self.c.Modes().MarkedBaseCommit.GetHash() != "",
self.c.Tr.RebasingFromBaseCommitTitle,
self.c.Tr.RebasingTitle),
map[string]string{
"checkedOutBranch": checkedOutBranch,
"ref": ref,
"checkedOutBranch": checkedOutBranchName,
},
)

Expand Down
14 changes: 9 additions & 5 deletions pkg/i18n/english.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,9 @@ type TranslationSet struct {
RebasingFromBaseCommitTitle string
SimpleRebase string
InteractiveRebase string
RebaseOntoBaseBranch string
InteractiveRebaseTooltip string
RebaseOntoBaseBranchTooltip string
MustSelectTodoCommits string
ConfirmMerge string
FwdNoUpstream string
Expand Down Expand Up @@ -1253,11 +1255,13 @@ func EnglishTranslationSet() TranslationSet {
KeybindingsMenuSectionLocal: "Local",
KeybindingsMenuSectionGlobal: "Global",
KeybindingsMenuSectionNavigation: "Navigation",
RebasingTitle: "Rebase '{{.checkedOutBranch}}' onto '{{.ref}}'",
RebasingFromBaseCommitTitle: "Rebase '{{.checkedOutBranch}}' from marked base onto '{{.ref}}'",
SimpleRebase: "Simple rebase",
InteractiveRebase: "Interactive rebase",
RebasingTitle: "Rebase '{{.checkedOutBranch}}'",
RebasingFromBaseCommitTitle: "Rebase '{{.checkedOutBranch}}' from marked base",
SimpleRebase: "Simple rebase onto '{{.ref}}'",
InteractiveRebase: "Interactive rebase onto '{{.ref}}'",
RebaseOntoBaseBranch: "Rebase onto base branch ({{.baseBranch}})",
InteractiveRebaseTooltip: "Begin an interactive rebase with a break at the start, so you can update the TODO commits before continuing.",
RebaseOntoBaseBranchTooltip: "Rebase the checked out branch onto its base branch (i.e. the closest main branch).",
MustSelectTodoCommits: "When rebasing, this action only works on a selection of TODO commits.",
ConfirmMerge: "Are you sure you want to merge '{{.selectedBranch}}' into '{{.checkedOutBranch}}'?",
FwdNoUpstream: "Cannot fast-forward a branch with no upstream",
Expand Down Expand Up @@ -1443,7 +1447,7 @@ func EnglishTranslationSet() TranslationSet {
ViewUpstreamResetOptions: "Reset checked-out branch onto {{.upstream}}",
ViewUpstreamResetOptionsTooltip: "View options for resetting the checked-out branch onto {{upstream}}. Note: this will not reset the selected branch onto the upstream, it will reset the checked-out branch onto the upstream.",
ViewUpstreamRebaseOptions: "Rebase checked-out branch onto {{.upstream}}",
ViewUpstreamRebaseOptionsTooltip: "View options for rebasing the checked-out branch onto {{upstream}}. Note: this will not rebase the selected branch onto the upstream, it will rebased the checked-out branch onto the upstream.",
ViewUpstreamRebaseOptionsTooltip: "View options for rebasing the checked-out branch onto {{upstream}}. Note: this will not rebase the selected branch onto the upstream, it will rebase the checked-out branch onto the upstream.",
UpstreamGenericName: "upstream of selected branch",
SetUpstreamTitle: "Set upstream branch",
SetUpstreamMessage: "Are you sure you want to set the upstream branch of '{{.checkedOut}}' to '{{.selected}}'",
Expand Down
8 changes: 4 additions & 4 deletions pkg/i18n/polish.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,10 +274,10 @@ func polishTranslationSet() TranslationSet {
KeybindingsMenuSectionLocal: "Lokalne",
KeybindingsMenuSectionGlobal: "Globalne",
KeybindingsMenuSectionNavigation: "Nawigacja",
RebasingTitle: "Rebase '{{.checkedOutBranch}}' na '{{.ref}}'",
RebasingFromBaseCommitTitle: "Rebase '{{.checkedOutBranch}}' od oznaczonego commita bazowego na '{{.ref}}'",
SimpleRebase: "Prosty rebase",
InteractiveRebase: "Interaktywny rebase",
RebasingTitle: "Rebase '{{.checkedOutBranch}}'",
RebasingFromBaseCommitTitle: "Rebase '{{.checkedOutBranch}}' od oznaczonego commita bazowego",
SimpleRebase: "Prosty rebase na '{{.ref}}'",
InteractiveRebase: "Interaktywny rebase na '{{.ref}}'",
InteractiveRebaseTooltip: "Rozpocznij interaktywny rebase z przerwaniem na początku, abyś mógł zaktualizować commity TODO przed kontynuacją.",
MustSelectTodoCommits: "Podczas rebase ta akcja działa tylko na zaznaczonych commitach TODO.",
ConfirmMerge: "Czy na pewno chcesz scalić '{{.selectedBranch}}' z '{{.checkedOutBranch}}'?",
Expand Down
6 changes: 3 additions & 3 deletions pkg/i18n/russian.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,9 @@ func RussianTranslationSet() TranslationSet {
ConflictsResolved: "Все конфликты слияния разрешены. Продолжить?",
Continue: "Продолжить",
Keybindings: "Связки клавиш",
RebasingTitle: "Перебазировать '{{.checkedOutBranch}}' на '{{.ref}}'",
SimpleRebase: "Простая перебазировка",
InteractiveRebase: "Интерактивная перебазировка",
RebasingTitle: "Перебазировать '{{.checkedOutBranch}}'",
SimpleRebase: "Простая перебазировка на '{{.ref}}'",
InteractiveRebase: "Интерактивная перебазировка на '{{.ref}}'",
InteractiveRebaseTooltip: "Начать интерактивную перебазировку с перерыва в начале, чтобы можно было обновить TODO коммиты, прежде чем продолжить.",
ConfirmMerge: "Вы уверены, что хотите to merge '{{.selectedBranch}}' into '{{.checkedOutBranch}}'?",
FwdNoUpstream: "Невозможно перемотать ветку без upstream-ветки",
Expand Down
6 changes: 3 additions & 3 deletions pkg/i18n/traditional_chinese.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,9 +256,9 @@ func traditionalChineseTranslationSet() TranslationSet {
ConflictsResolved: "所有合併衝突都已解決。是否繼續?",
Continue: "確認",
Keybindings: "鍵盤快捷鍵",
RebasingTitle: "將 '{{.checkedOutBranch}}' 變基至 '{{.ref}}'",
SimpleRebase: "簡單變基",
InteractiveRebase: "互動變基",
RebasingTitle: "將 '{{.checkedOutBranch}}'",
SimpleRebase: "簡單變基 變基至 '{{.ref}}'",
InteractiveRebase: "互動變基 變基至 '{{.ref}}'",
InteractiveRebaseTooltip: "開始一個互動變基,以中斷開始,這樣你可以在繼續之前更新TODO提交",
ConfirmMerge: "是否將 '{{.selectedBranch}}' 合併至 '{{.checkedOutBranch}}' ?",
FwdNoUpstream: "無法快進無上游分支",
Expand Down
2 changes: 1 addition & 1 deletion pkg/integration/tests/branch/rebase.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ var Rebase = NewIntegrationTest(NewIntegrationTestArgs{
Press(keys.Branches.RebaseBranch)

t.ExpectPopup().Menu().
Title(Equals("Rebase 'first-change-branch' onto 'second-change-branch'")).
Title(Equals("Rebase 'first-change-branch'")).
Select(Contains("Simple rebase")).
Confirm()

Expand Down
2 changes: 1 addition & 1 deletion pkg/integration/tests/branch/rebase_abort_on_conflict.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ var RebaseAbortOnConflict = NewIntegrationTest(NewIntegrationTestArgs{
Press(keys.Branches.RebaseBranch)

t.ExpectPopup().Menu().
Title(Equals("Rebase 'first-change-branch' onto 'second-change-branch'")).
Title(Equals("Rebase 'first-change-branch'")).
Select(Contains("Simple rebase")).
Confirm()

Expand Down
2 changes: 1 addition & 1 deletion pkg/integration/tests/branch/rebase_and_drop.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ var RebaseAndDrop = NewIntegrationTest(NewIntegrationTestArgs{
Press(keys.Branches.RebaseBranch)

t.ExpectPopup().Menu().
Title(Equals("Rebase 'first-change-branch' onto 'second-change-branch'")).
Title(Equals("Rebase 'first-change-branch'")).
Select(Contains("Simple rebase")).
Confirm()

Expand Down
2 changes: 1 addition & 1 deletion pkg/integration/tests/branch/rebase_cancel_on_conflict.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ var RebaseCancelOnConflict = NewIntegrationTest(NewIntegrationTestArgs{
Press(keys.Branches.RebaseBranch)

t.ExpectPopup().Menu().
Title(Equals("Rebase 'first-change-branch' onto 'second-change-branch'")).
Title(Equals("Rebase 'first-change-branch'")).
Select(Contains("Simple rebase")).
Confirm()

Expand Down
2 changes: 1 addition & 1 deletion pkg/integration/tests/branch/rebase_copied_branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ var RebaseCopiedBranch = NewIntegrationTest(NewIntegrationTestArgs{
Press(keys.Branches.RebaseBranch).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Rebase 'branch2' onto 'master'")).
Title(Equals("Rebase 'branch2'")).
Select(Contains("Simple rebase")).
Confirm()
})
Expand Down
2 changes: 1 addition & 1 deletion pkg/integration/tests/branch/rebase_does_not_autosquash.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ var RebaseDoesNotAutosquash = NewIntegrationTest(NewIntegrationTestArgs{
Press(keys.Branches.RebaseBranch)

t.ExpectPopup().Menu().
Title(Equals("Rebase 'my-branch' onto 'master'")).
Title(Equals("Rebase 'my-branch'")).
Select(Contains("Simple rebase")).
Confirm()

Expand Down
2 changes: 1 addition & 1 deletion pkg/integration/tests/branch/rebase_from_marked_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ var RebaseFromMarkedBase = NewIntegrationTest(NewIntegrationTestArgs{
Press(keys.Branches.RebaseBranch)

t.ExpectPopup().Menu().
Title(Equals("Rebase 'active-branch' from marked base onto 'target-branch'")).
Title(Equals("Rebase 'active-branch' from marked base")).
Select(Contains("Simple rebase")).
Confirm()

Expand Down
53 changes: 53 additions & 0 deletions pkg/integration/tests/branch/rebase_onto_base_branch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package branch

import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)

var RebaseOntoBaseBranch = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Rebase the current branch onto its base branch",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.UserConfig.Gui.ShowDivergenceFromBaseBranch = "arrowAndNumber"
},
SetupRepo: func(shell *Shell) {
shell.
EmptyCommit("master 1").
EmptyCommit("master 2").
EmptyCommit("master 3").
NewBranchFrom("feature", "master^").
EmptyCommit("feature 1").
EmptyCommit("feature 2")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().Lines(
Contains("feature 2"),
Contains("feature 1"),
Contains("master 2"),
Contains("master 1"),
)

t.Views().Branches().
Focus().
Lines(
Contains("feature ↓1").IsSelected(),
Contains("master"),
).
Press(keys.Branches.RebaseBranch)

t.ExpectPopup().Menu().
Title(Equals("Rebase 'feature'")).
Select(Contains("Rebase onto base branch (master)")).
Confirm()

t.Views().Commits().Lines(
Contains("feature 2"),
Contains("feature 1"),
Contains("master 3"),
Contains("master 2"),
Contains("master 1"),
)
},
})
2 changes: 1 addition & 1 deletion pkg/integration/tests/branch/rebase_to_upstream.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ var RebaseToUpstream = NewIntegrationTest(NewIntegrationTestArgs{
Select(Contains("Rebase checked-out branch onto origin/master...")).
Confirm()
t.ExpectPopup().Menu().
Title(Equals("Rebase 'target' onto 'origin/master'")).
Title(Equals("Rebase 'target'")).
Select(Contains("Simple rebase")).
Confirm()
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ var AdvancedInteractiveRebase = NewIntegrationTest(NewIntegrationTestArgs{
Press(keys.Branches.RebaseBranch)

t.ExpectPopup().Menu().
Title(Equals(fmt.Sprintf("Rebase '%s' onto '%s'", TOP_BRANCH, BASE_BRANCH))).
Title(Equals(fmt.Sprintf("Rebase '%s'", TOP_BRANCH))).
Select(Contains("Interactive rebase")).
Confirm()
t.Views().Commits().
Expand Down
1 change: 1 addition & 0 deletions pkg/integration/tests/test_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ var tests = []*components.IntegrationTest{
branch.RebaseCopiedBranch,
branch.RebaseDoesNotAutosquash,
branch.RebaseFromMarkedBase,
branch.RebaseOntoBaseBranch,
branch.RebaseToUpstream,
branch.Rename,
branch.Reset,
Expand Down

0 comments on commit b856877

Please sign in to comment.