Skip to content

Commit

Permalink
pkg/gui: Allow user to select remote and branch when creating a PR
Browse files Browse the repository at this point in the history
When creating a PR against a selected branch (via O = "create pull request
options"), the user will first be asked to select a remote (if there is more
than one). After that, the suggestion area is populated with all remote branches
at that origin - instead of all local ones. After all, creating a PR against a
branch that doesn't exist on the remote won't work.

Please note that the "PR is not filed against 'origin' remote" use cases (e.g. when
contributing via a fork that is 'origin' to a GitHub project that is
'upstream'), is not working correctly yet.

Fixes #1826.
  • Loading branch information
moha-gh committed Nov 20, 2024
1 parent b62546c commit e0934db
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 4 deletions.
40 changes: 36 additions & 4 deletions pkg/gui/controllers/branches_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -730,11 +730,23 @@ func (self *BranchesController) createPullRequestMenu(selectedBranch *models.Bra
{
LabelColumns: fromToLabelColumns(branch.Name, self.c.Tr.SelectBranch),
OnPress: func() error {
if !branch.IsTrackingRemote() {
return errors.New(self.c.Tr.PullRequestNoUpstream)
}

if len(self.c.Model().Remotes) == 1 {
toRemote := self.c.Model().Remotes[0].Name
self.c.Log.Debugf("PR will target the only existing remote '%s'", toRemote)
return self.promptForTargetBranchNameAndCreatePullRequest(branch, toRemote)
}

self.c.Prompt(types.PromptOpts{
Title: branch.Name + " →",
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRemoteBranchesSuggestionsFunc("/"),
HandleConfirm: func(targetBranchName string) error {
return self.createPullRequest(branch.Name, targetBranchName)
Title: self.c.Tr.SelectTargetRemote,
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRemoteSuggestionsFunc(),
HandleConfirm: func(toRemote string) error {
self.c.Log.Debugf("PR will target remote '%s'", toRemote)

return self.promptForTargetBranchNameAndCreatePullRequest(branch, toRemote)
},
})

Expand Down Expand Up @@ -764,6 +776,26 @@ func (self *BranchesController) createPullRequestMenu(selectedBranch *models.Bra
return self.c.Menu(types.CreateMenuOptions{Title: fmt.Sprint(self.c.Tr.CreatePullRequestOptions), Items: menuItems})
}

func (self *BranchesController) promptForTargetBranchNameAndCreatePullRequest(fromBranch *models.Branch, toRemote string) error {
remoteDoesNotExist := lo.NoneBy(self.c.Model().Remotes, func(remote *models.Remote) bool {
return remote.Name == toRemote
})
if remoteDoesNotExist {
return fmt.Errorf(self.c.Tr.NoValidRemoteName, toRemote)
}

self.c.Prompt(types.PromptOpts{
Title: fmt.Sprintf("%s → %s/", fromBranch.UpstreamBranch, toRemote),
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRemoteBranchesForRemoteSuggestionsFunc(toRemote),
HandleConfirm: func(toBranch string) error {
self.c.Log.Debugf("PR will target branch '%s' on remote '%s'", toBranch, toRemote)
return self.createPullRequest(fromBranch.UpstreamBranch, toBranch)
},
})

return nil
}

func (self *BranchesController) createPullRequest(from string, to string) error {
url, err := self.c.Helpers().Host.GetPullRequestURL(from, to)
if err != nil {
Expand Down
16 changes: 16 additions & 0 deletions pkg/gui/controllers/helpers/suggestions_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,26 @@ func (self *SuggestionsHelper) getRemoteBranchNames(separator string) []string {
})
}

func (self *SuggestionsHelper) getRemoteBranchNamesForRemote(remoteName string) []string {
remote, ok := lo.Find(self.c.Model().Remotes, func(remote *models.Remote) bool {
return remote.Name == remoteName
})
if ok {
return lo.Map(remote.Branches, func(branch *models.RemoteBranch, _ int) string {
return branch.Name
})
}
return nil
}

func (self *SuggestionsHelper) GetRemoteBranchesSuggestionsFunc(separator string) func(string) []*types.Suggestion {
return FilterFunc(self.getRemoteBranchNames(separator), self.c.UserConfig().Gui.UseFuzzySearch())
}

func (self *SuggestionsHelper) GetRemoteBranchesForRemoteSuggestionsFunc(remoteName string) func(string) []*types.Suggestion {
return FilterFunc(self.getRemoteBranchNamesForRemote(remoteName), self.c.UserConfig().Gui.UseFuzzySearch())
}

func (self *SuggestionsHelper) getTagNames() []string {
return lo.Map(self.c.Model().Tags, func(tag *models.Tag, _ int) string {
return tag.Name
Expand Down
4 changes: 4 additions & 0 deletions pkg/i18n/english.go
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,8 @@ type TranslationSet struct {
CreatePullRequestOptions string
DefaultBranch string
SelectBranch string
SelectTargetRemote string
NoValidRemoteName string
CreatePullRequest string
SelectConfigFile string
NoConfigFileFoundErr string
Expand Down Expand Up @@ -1676,6 +1678,8 @@ func EnglishTranslationSet() *TranslationSet {
CreatePullRequestOptions: "View create pull request options",
DefaultBranch: "Default branch",
SelectBranch: "Select branch",
SelectTargetRemote: "Select target remote",
NoValidRemoteName: "A remote named '%s' does not exist",
SelectConfigFile: "Select config file",
NoConfigFileFoundErr: "No config file found",
LoadingFileSuggestions: "Loading file suggestions",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package branch

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

var OpenPullRequestInvalidTargetRemoteName = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Open up a pull request, specifying a non-existing target remote",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
// Create an initial commit ('git branch set-upstream-to' bails out otherwise)
shell.CreateFileAndAdd("file", "content1")
shell.Commit("one")

// Create a new branch
shell.NewBranch("branch-1")

// Create a couple of remotes
shell.CloneIntoRemote("upstream")
shell.CloneIntoRemote("origin")

// To allow a pull request to be created from a branch, it must have an upstream set.
shell.SetBranchUpstream("branch-1", "origin/branch-1")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
// Open a PR for the current branch (i.e. 'branch-1')
t.Views().
Branches().
Focus().
Press(keys.Branches.ViewPullRequestOptions)

t.ExpectPopup().
Menu().
Title(Equals("View create pull request options")).
Select(Contains("Select branch")).
Confirm()

// Verify that we're prompted to enter the remote and enter the name of a non-existing one.
t.ExpectPopup().
Prompt().
Title(Equals("Select target remote")).
Type("non-existing-remote").
Confirm()

// Verify that this leads to an error being shown (instead of progressing to branch selection).
t.ExpectPopup().Alert().
Title(Equals("Error")).
Content(Contains("A remote named 'non-existing-remote' does not exist")).
Confirm()
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package branch

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

var OpenPullRequestSelectRemoteAndTargetBranch = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Open up a pull request, specifying a remote and target branch",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.GetUserConfig().OS.OpenLink = "echo {{link}} > /tmp/openlink"
},
SetupRepo: func(shell *Shell) {
// Create an initial commit ('git branch set-upstream-to' bails out otherwise)
shell.CreateFileAndAdd("file", "content1")
shell.Commit("one")

// Create a new branch and a remote that has that branch
shell.NewBranch("branch-1")
shell.CloneIntoRemote("upstream")

// Create another branch and a second remote. The first remote doesn't have this branch.
shell.NewBranch("branch-2")
shell.CloneIntoRemote("origin")

// To allow a pull request to be created from a branch, it must have an upstream set.
shell.SetBranchUpstream("branch-2", "origin/branch-2")

shell.RunCommand([]string{"git", "remote", "set-url", "origin", "https://github.com/my-personal-fork/lazygit"})
shell.RunCommand([]string{"git", "remote", "set-url", "upstream", "https://github.com/jesseduffield/lazygit"})
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
// Open a PR for the current branch (i.e. 'branch-2')
t.Views().
Branches().
Focus().
Press(keys.Branches.ViewPullRequestOptions)

t.ExpectPopup().
Menu().
Title(Equals("View create pull request options")).
Select(Contains("Select branch")).
Confirm()

// Verify that we're prompted to enter the remote
t.ExpectPopup().
Prompt().
Title(Equals("Select target remote")).
SuggestionLines(
Equals("origin"),
Equals("upstream")).
ConfirmSuggestion(Equals("upstream"))

// Verify that we're prompted to enter the target branch and that only those branches
// present in the selected remote are listed as suggestions (i.e. 'branch-2' is not there).
t.ExpectPopup().
Prompt().
Title(Equals("branch-2 → upstream/")).
SuggestionLines(
Equals("branch-1"),
Equals("master")).
ConfirmSuggestion(Equals("master"))

// Verify that the expected URL is used (by checking the openlink file)
// TODO: How exactly to best do that?
t.Shell().RunCommand([]string{"cat", "/tmp/openlink"})
},
})
2 changes: 2 additions & 0 deletions pkg/integration/tests/test_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ var tests = []*components.IntegrationTest{
branch.NewBranchAutostash,
branch.NewBranchFromRemoteTrackingDifferentName,
branch.NewBranchFromRemoteTrackingSameName,
branch.OpenPullRequestInvalidTargetRemoteName,
branch.OpenPullRequestNoUpstream,
branch.OpenPullRequestSelectRemoteAndTargetBranch,
branch.OpenWithCliArg,
branch.Rebase,
branch.RebaseAbortOnConflict,
Expand Down

0 comments on commit e0934db

Please sign in to comment.