mirror of
https://github.com/jesseduffield/lazygit.git
synced 2024-11-26 09:00:57 +02:00
Merge pull request #15 from jesseduffield/master
Update to latest master
This commit is contained in:
commit
6f2b62f729
@ -69,6 +69,12 @@ and the git version which builds from the most recent commit.
|
||||
Instruction of how to install AUR content can be found here:
|
||||
https://wiki.archlinux.org/index.php/Arch_User_Repository
|
||||
|
||||
### Conda
|
||||
Released versions are available for different platforms, see https://anaconda.org/conda-forge/lazygit
|
||||
```sh
|
||||
conda install -c conda-forge lazygit
|
||||
```
|
||||
|
||||
### Binary Release (Windows/Linux/OSX)
|
||||
You can download a binary release [here](https://github.com/jesseduffield/lazygit/releases).
|
||||
|
||||
|
@ -555,6 +555,22 @@ func (c *GitCommand) Show(sha string) (string, error) {
|
||||
return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git show --color %s", sha))
|
||||
}
|
||||
|
||||
// GetRemoteURL returns current repo remote url
|
||||
func (c *GitCommand) GetRemoteURL() string {
|
||||
url, _ := c.OSCommand.RunCommandWithOutput("git config --get remote.origin.url")
|
||||
return utils.TrimTrailingNewline(url)
|
||||
}
|
||||
|
||||
// CheckRemoteBranchExists Returns remote branch
|
||||
func (c *GitCommand) CheckRemoteBranchExists(branch *Branch) bool {
|
||||
_, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf(
|
||||
"git show-ref --verify -- refs/remotes/origin/%s",
|
||||
branch.Name,
|
||||
))
|
||||
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Diff returns the diff of a file
|
||||
func (c *GitCommand) Diff(file *File) string {
|
||||
cachedArg := ""
|
||||
|
@ -8,9 +8,7 @@ import (
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
|
||||
"github.com/mgutz/str"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
gitconfig "github.com/tcnksm/go-gitconfig"
|
||||
)
|
||||
@ -22,6 +20,7 @@ type Platform struct {
|
||||
shellArg string
|
||||
escapedQuote string
|
||||
openCommand string
|
||||
openLinkCommand string
|
||||
fallbackEscapedQuote string
|
||||
}
|
||||
|
||||
@ -110,6 +109,18 @@ func (c *OSCommand) OpenFile(filename string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// OpenFile opens a file with the given
|
||||
func (c *OSCommand) OpenLink(link string) error {
|
||||
commandTemplate := c.Config.GetUserConfig().GetString("os.openLinkCommand")
|
||||
templateValues := map[string]string{
|
||||
"link": c.Quote(link),
|
||||
}
|
||||
|
||||
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
|
||||
err := c.RunCommand(command)
|
||||
return err
|
||||
}
|
||||
|
||||
// EditFile opens a file in a subprocess using whatever editor is available,
|
||||
// falling back to core.editor, VISUAL, EDITOR, then vi
|
||||
func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
|
||||
|
@ -13,6 +13,7 @@ func getPlatform() *Platform {
|
||||
shellArg: "-c",
|
||||
escapedQuote: "'",
|
||||
openCommand: "open {{filename}}",
|
||||
openLinkCommand: "open {{link}}",
|
||||
fallbackEscapedQuote: "\"",
|
||||
}
|
||||
}
|
||||
|
105
pkg/commands/pull_request.go
Normal file
105
pkg/commands/pull_request.go
Normal file
@ -0,0 +1,105 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Service is a service that repository is on (Github, Bitbucket, ...)
|
||||
type Service struct {
|
||||
Name string
|
||||
PullRequestURL string
|
||||
}
|
||||
|
||||
// PullRequest opens a link in browser to create new pull request
|
||||
// with selected branch
|
||||
type PullRequest struct {
|
||||
GitServices []*Service
|
||||
GitCommand *GitCommand
|
||||
}
|
||||
|
||||
// RepoInformation holds some basic information about the repo
|
||||
type RepoInformation struct {
|
||||
Owner string
|
||||
Repository string
|
||||
}
|
||||
|
||||
func getServices() []*Service {
|
||||
return []*Service{
|
||||
{
|
||||
Name: "github.com",
|
||||
PullRequestURL: "https://github.com/%s/%s/compare/%s?expand=1",
|
||||
},
|
||||
{
|
||||
Name: "bitbucket.org",
|
||||
PullRequestURL: "https://bitbucket.org/%s/%s/pull-requests/new?t=%s",
|
||||
},
|
||||
{
|
||||
Name: "gitlab.com",
|
||||
PullRequestURL: "https://gitlab.com/%s/%s/merge_requests/new?merge_request[source_branch]=%s",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewPullRequest creates new instance of PullRequest
|
||||
func NewPullRequest(gitCommand *GitCommand) *PullRequest {
|
||||
return &PullRequest{
|
||||
GitServices: getServices(),
|
||||
GitCommand: gitCommand,
|
||||
}
|
||||
}
|
||||
|
||||
// Create opens link to new pull request in browser
|
||||
func (pr *PullRequest) Create(branch *Branch) error {
|
||||
branchExistsOnRemote := pr.GitCommand.CheckRemoteBranchExists(branch)
|
||||
|
||||
if !branchExistsOnRemote {
|
||||
return errors.New(pr.GitCommand.Tr.SLocalize("NoBranchOnRemote"))
|
||||
}
|
||||
|
||||
repoURL := pr.GitCommand.GetRemoteURL()
|
||||
var gitService *Service
|
||||
|
||||
for _, service := range pr.GitServices {
|
||||
if strings.Contains(repoURL, service.Name) {
|
||||
gitService = service
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if gitService == nil {
|
||||
return errors.New(pr.GitCommand.Tr.SLocalize("UnsupportedGitService"))
|
||||
}
|
||||
|
||||
repoInfo := getRepoInfoFromURL(repoURL)
|
||||
|
||||
return pr.GitCommand.OSCommand.OpenLink(fmt.Sprintf(
|
||||
gitService.PullRequestURL, repoInfo.Owner, repoInfo.Repository, branch.Name,
|
||||
))
|
||||
}
|
||||
|
||||
func getRepoInfoFromURL(url string) *RepoInformation {
|
||||
isHTTP := strings.HasPrefix(url, "http")
|
||||
|
||||
if isHTTP {
|
||||
splits := strings.Split(url, "/")
|
||||
owner := splits[len(splits)-2]
|
||||
repo := strings.TrimSuffix(splits[len(splits)-1], ".git")
|
||||
|
||||
return &RepoInformation{
|
||||
Owner: owner,
|
||||
Repository: repo,
|
||||
}
|
||||
}
|
||||
|
||||
tmpSplit := strings.Split(url, ":")
|
||||
splits := strings.Split(tmpSplit[1], "/")
|
||||
owner := splits[0]
|
||||
repo := strings.TrimSuffix(splits[1], ".git")
|
||||
|
||||
return &RepoInformation{
|
||||
Owner: owner,
|
||||
Repository: repo,
|
||||
}
|
||||
}
|
152
pkg/commands/pull_request_test.go
Normal file
152
pkg/commands/pull_request_test.go
Normal file
@ -0,0 +1,152 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetRepoInfoFromURL(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
repoURL string
|
||||
test func(*RepoInformation)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Returns repository information for git remote url",
|
||||
"git@github.com:petersmith/super_calculator",
|
||||
func(repoInfo *RepoInformation) {
|
||||
assert.EqualValues(t, repoInfo.Owner, "petersmith")
|
||||
assert.EqualValues(t, repoInfo.Repository, "super_calculator")
|
||||
},
|
||||
},
|
||||
{
|
||||
"Returns repository information for http remote url",
|
||||
"https://my_username@bitbucket.org/johndoe/social_network.git",
|
||||
func(repoInfo *RepoInformation) {
|
||||
assert.EqualValues(t, repoInfo.Owner, "johndoe")
|
||||
assert.EqualValues(t, repoInfo.Repository, "social_network")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
s.test(getRepoInfoFromURL(s.repoURL))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatePullRequest(t *testing.T) {
|
||||
type scenario struct {
|
||||
testName string
|
||||
branch *Branch
|
||||
command func(string, ...string) *exec.Cmd
|
||||
test func(err error)
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
"Opens a link to new pull request on bitbucket",
|
||||
&Branch{
|
||||
Name: "feature/profile-page",
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return exec.Command("echo", "git@bitbucket.org:johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?t=feature/profile-page"})
|
||||
return exec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Opens a link to new pull request on bitbucket with http remote url",
|
||||
&Branch{
|
||||
Name: "feature/events",
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return exec.Command("echo", "https://my_username@bitbucket.org/johndoe/social_network.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://bitbucket.org/johndoe/social_network/pull-requests/new?t=feature/events"})
|
||||
return exec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Opens a link to new pull request on github",
|
||||
&Branch{
|
||||
Name: "feature/sum-operation",
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return exec.Command("echo", "git@github.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://github.com/peter/calculator/compare/feature/sum-operation?expand=1"})
|
||||
return exec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Opens a link to new pull request on gitlab",
|
||||
&Branch{
|
||||
Name: "feature/ui",
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
// Handle git remote url call
|
||||
if strings.HasPrefix(cmd, "git") {
|
||||
return exec.Command("echo", "git@gitlab.com:peter/calculator.git")
|
||||
}
|
||||
|
||||
assert.Equal(t, cmd, "open")
|
||||
assert.Equal(t, args, []string{"https://gitlab.com/peter/calculator/merge_requests/new?merge_request[source_branch]=feature/ui"})
|
||||
return exec.Command("echo")
|
||||
},
|
||||
func(err error) {
|
||||
assert.NoError(t, err)
|
||||
},
|
||||
},
|
||||
{
|
||||
"Throws an error if git service is unsupported",
|
||||
&Branch{
|
||||
Name: "feature/divide-operation",
|
||||
},
|
||||
func(cmd string, args ...string) *exec.Cmd {
|
||||
return exec.Command("echo", "git@something.com:peter/calculator.git")
|
||||
},
|
||||
func(err error) {
|
||||
assert.Error(t, err)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
gitCommand := newDummyGitCommand()
|
||||
gitCommand.OSCommand.command = s.command
|
||||
gitCommand.OSCommand.Config.GetUserConfig().Set("os.openLinkCommand", "open {{link}}")
|
||||
dummyPullRequest := NewPullRequest(gitCommand)
|
||||
s.test(dummyPullRequest.Create(s.branch))
|
||||
})
|
||||
}
|
||||
}
|
@ -6,5 +6,6 @@ package config
|
||||
func GetPlatformDefaultConfig() []byte {
|
||||
return []byte(
|
||||
`os:
|
||||
openCommand: 'open {{filename}}'`)
|
||||
openCommand: 'open {{filename}}'
|
||||
openLinkCommand: 'open {{link}}'`)
|
||||
}
|
||||
|
@ -4,5 +4,6 @@ package config
|
||||
func GetPlatformDefaultConfig() []byte {
|
||||
return []byte(
|
||||
`os:
|
||||
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'`)
|
||||
openCommand: 'sh -c "xdg-open {{filename}} >/dev/null"'
|
||||
openLinkCommand: 'sh -c "xdg-open {{link}} >/dev/null"'`)
|
||||
}
|
||||
|
@ -4,5 +4,6 @@ package config
|
||||
func GetPlatformDefaultConfig() []byte {
|
||||
return []byte(
|
||||
`os:
|
||||
openCommand: 'cmd /c "start "" {{filename}}"'`)
|
||||
openCommand: 'cmd /c "start "" {{filename}}"'
|
||||
openLinkCommand: 'cmd /c "start "" {{link}}"'`)
|
||||
}
|
||||
|
@ -22,6 +22,17 @@ func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
|
||||
return gui.refreshSidePanels(g)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleCreatePullRequestPress(g *gocui.Gui, v *gocui.View) error {
|
||||
branch := gui.getSelectedBranch(gui.getBranchesView(g))
|
||||
pullRequest := commands.NewPullRequest(gui.GitCommand)
|
||||
|
||||
if err := pullRequest.Create(branch); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
|
||||
branch := gui.getSelectedBranch(v)
|
||||
message := gui.Tr.SLocalize("SureForceCheckout")
|
||||
@ -76,6 +87,10 @@ func (gui *Gui) deleteBranch(g *gocui.Gui, v *gocui.View, force bool) error {
|
||||
if checkedOutBranch.Name == selectedBranch.Name {
|
||||
return gui.createErrorPanel(g, gui.Tr.SLocalize("CantDeleteCheckOutBranch"))
|
||||
}
|
||||
return gui.deleteNamedBranch(g, v, selectedBranch, force)
|
||||
}
|
||||
|
||||
func (gui *Gui) deleteNamedBranch(g *gocui.Gui, v *gocui.View, selectedBranch *commands.Branch, force bool) error {
|
||||
title := gui.Tr.SLocalize("DeleteBranch")
|
||||
var messageId string
|
||||
if force {
|
||||
@ -91,7 +106,12 @@ func (gui *Gui) deleteBranch(g *gocui.Gui, v *gocui.View, force bool) error {
|
||||
)
|
||||
return gui.createConfirmationPanel(g, v, title, message, func(g *gocui.Gui, v *gocui.View) error {
|
||||
if err := gui.GitCommand.DeleteBranch(selectedBranch.Name, force); err != nil {
|
||||
return gui.createErrorPanel(g, err.Error())
|
||||
errMessage := err.Error()
|
||||
if !force && strings.Contains(errMessage, "is not fully merged") {
|
||||
return gui.deleteNamedBranch(g, v, selectedBranch, true)
|
||||
} else {
|
||||
return gui.createErrorPanel(g, errMessage)
|
||||
}
|
||||
}
|
||||
return gui.refreshSidePanels(g)
|
||||
}, nil)
|
||||
|
@ -277,6 +277,12 @@ func (gui *Gui) GetKeybindings() []*Binding {
|
||||
Handler: gui.handleBranchPress,
|
||||
KeyReadable: "space",
|
||||
Description: gui.Tr.SLocalize("checkout"),
|
||||
}, {
|
||||
ViewName: "branches",
|
||||
Key: 'o',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleCreatePullRequestPress,
|
||||
Description: gui.Tr.SLocalize("createPullRequest"),
|
||||
}, {
|
||||
ViewName: "branches",
|
||||
Key: 'c',
|
||||
@ -301,12 +307,6 @@ func (gui *Gui) GetKeybindings() []*Binding {
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleDeleteBranch,
|
||||
Description: gui.Tr.SLocalize("deleteBranch"),
|
||||
}, {
|
||||
ViewName: "branches",
|
||||
Key: 'D',
|
||||
Modifier: gocui.ModNone,
|
||||
Handler: gui.handleForceDeleteBranch,
|
||||
Description: gui.Tr.SLocalize("forceDeleteBranch"),
|
||||
}, {
|
||||
ViewName: "branches",
|
||||
Key: 'm',
|
||||
|
@ -379,6 +379,15 @@ func addDutch(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "ConfirmQuit",
|
||||
Other: `Weet je zeker dat je dit programma wil sluiten?`,
|
||||
}, &i18n.Message{
|
||||
ID: "UnsupportedGitService",
|
||||
Other: `Niet-ondersteunde git-service`,
|
||||
}, &i18n.Message{
|
||||
ID: "createPullRequest",
|
||||
Other: `maak een pull-aanvraag`,
|
||||
}, &i18n.Message{
|
||||
ID: "NoBranchOnRemote",
|
||||
Other: `Deze tak bestaat niet op de afstandsbediening. U moet eerst op de afstandsbediening drukken.`,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -176,7 +176,7 @@ func addEnglish(i18nObject *i18n.Bundle) error {
|
||||
Other: "Are you sure you want to delete the branch {{.selectedBranchName}}?",
|
||||
}, &i18n.Message{
|
||||
ID: "ForceDeleteBranchMessage",
|
||||
Other: "Are you sure you want to force delete the branch {{.selectedBranchName}}?",
|
||||
Other: "{{.selectedBranchName}} is not fully merged. Are you sure you want to delete it?",
|
||||
}, &i18n.Message{
|
||||
ID: "CantMergeBranchIntoItself",
|
||||
Other: "You cannot merge a branch into itself",
|
||||
@ -402,6 +402,15 @@ func addEnglish(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "SwitchRepo",
|
||||
Other: `switch to a recent repo`,
|
||||
}, &i18n.Message{
|
||||
ID: "UnsupportedGitService",
|
||||
Other: `Unsupported git service`,
|
||||
}, &i18n.Message{
|
||||
ID: "createPullRequest",
|
||||
Other: `create pull request`,
|
||||
}, &i18n.Message{
|
||||
ID: "NoBranchOnRemote",
|
||||
Other: `This branch doesn't exist on remote. You need to push it to remote first.`,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -377,6 +377,15 @@ func addPolish(i18nObject *i18n.Bundle) error {
|
||||
}, &i18n.Message{
|
||||
ID: "ConfirmQuit",
|
||||
Other: `Na pewno chcesz wyjść z programu?`,
|
||||
}, &i18n.Message{
|
||||
ID: "UnsupportedGitService",
|
||||
Other: `Nieobsługiwana usługa git`,
|
||||
}, &i18n.Message{
|
||||
ID: "createPullRequest",
|
||||
Other: `utwórz żądanie wyciągnięcia`,
|
||||
}, &i18n.Message{
|
||||
ID: "NoBranchOnRemote",
|
||||
Other: `Ta gałąź nie istnieje na zdalnym. Najpierw musisz go odepchnąć na odległość.`,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user