mirror of
https://github.com/jesseduffield/lazygit.git
synced 2024-11-26 09:00:57 +02:00
progress on refactor
This commit is contained in:
parent
f9c39ad64b
commit
97cff65612
@ -4,8 +4,10 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||||
"github.com/jesseduffield/lazygit/pkg/config"
|
"github.com/jesseduffield/lazygit/pkg/config"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/gui"
|
||||||
)
|
)
|
||||||
|
|
||||||
// App struct
|
// App struct
|
||||||
@ -16,6 +18,7 @@ type App struct {
|
|||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
OSCommand *commands.OSCommand
|
OSCommand *commands.OSCommand
|
||||||
GitCommand *commands.GitCommand
|
GitCommand *commands.GitCommand
|
||||||
|
Gui *gocui.Gui
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewApp retruns a new applications
|
// NewApp retruns a new applications
|
||||||
@ -34,6 +37,10 @@ func NewApp(config config.AppConfigurer) (*App, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, config.GetVersion())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,23 @@
|
|||||||
package git
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Branch : A git branch
|
||||||
|
// duplicating this for now
|
||||||
|
type Branch struct {
|
||||||
|
Name string
|
||||||
|
Recency string
|
||||||
|
}
|
||||||
|
|
||||||
// GetDisplayString returns the dispaly string of branch
|
// GetDisplayString returns the dispaly string of branch
|
||||||
// func (b *Branch) GetDisplayString() string {
|
func (b *Branch) GetDisplayString() string {
|
||||||
// return gui.withPadding(b.Recency, 4) + gui.coloredString(b.Name, b.getColor())
|
return utils.WithPadding(b.Recency, 4) + utils.ColoredString(b.Name, b.GetColor())
|
||||||
// }
|
}
|
||||||
|
|
||||||
// GetColor branch color
|
// GetColor branch color
|
||||||
func (b *Branch) GetColor() color.Attribute {
|
func (b *Branch) GetColor() color.Attribute {
|
@ -38,13 +38,6 @@ func (c *GitCommand) SetupGit() {
|
|||||||
c.setupWorktree()
|
c.setupWorktree()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GitIgnore adds a file to the .gitignore of the repo
|
|
||||||
func (c *GitCommand) GitIgnore(filename string) {
|
|
||||||
if _, err := c.OSCommand.RunDirectCommand("echo '" + filename + "' >> .gitignore"); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStashEntries stash entryies
|
// GetStashEntries stash entryies
|
||||||
func (c *GitCommand) GetStashEntries() []StashEntry {
|
func (c *GitCommand) GetStashEntries() []StashEntry {
|
||||||
stashEntries := make([]StashEntry, 0)
|
stashEntries := make([]StashEntry, 0)
|
||||||
@ -78,10 +71,10 @@ func includes(array []string, str string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetStatusFiles git status files
|
// GetStatusFiles git status files
|
||||||
func (c *GitCommand) GetStatusFiles() []GitFile {
|
func (c *GitCommand) GetStatusFiles() []File {
|
||||||
statusOutput, _ := c.GitStatus()
|
statusOutput, _ := c.GitStatus()
|
||||||
statusStrings := utils.SplitLines(statusOutput)
|
statusStrings := utils.SplitLines(statusOutput)
|
||||||
gitFiles := make([]GitFile, 0)
|
files := make([]File, 0)
|
||||||
|
|
||||||
for _, statusString := range statusStrings {
|
for _, statusString := range statusStrings {
|
||||||
change := statusString[0:2]
|
change := statusString[0:2]
|
||||||
@ -89,7 +82,7 @@ func (c *GitCommand) GetStatusFiles() []GitFile {
|
|||||||
unstagedChange := statusString[1:2]
|
unstagedChange := statusString[1:2]
|
||||||
filename := statusString[3:]
|
filename := statusString[3:]
|
||||||
tracked := !includes([]string{"??", "A "}, change)
|
tracked := !includes([]string{"??", "A "}, change)
|
||||||
gitFile := GitFile{
|
file := File{
|
||||||
Name: filename,
|
Name: filename,
|
||||||
DisplayString: statusString,
|
DisplayString: statusString,
|
||||||
HasStagedChanges: !includes([]string{" ", "U", "?"}, stagedChange),
|
HasStagedChanges: !includes([]string{" ", "U", "?"}, stagedChange),
|
||||||
@ -98,10 +91,10 @@ func (c *GitCommand) GetStatusFiles() []GitFile {
|
|||||||
Deleted: unstagedChange == "D" || stagedChange == "D",
|
Deleted: unstagedChange == "D" || stagedChange == "D",
|
||||||
HasMergeConflicts: change == "UU",
|
HasMergeConflicts: change == "UU",
|
||||||
}
|
}
|
||||||
gitFiles = append(gitFiles, gitFile)
|
files = append(files, file)
|
||||||
}
|
}
|
||||||
c.Log.Info(gitFiles) // TODO: use a dumper-esque log here
|
c.Log.Info(files) // TODO: use a dumper-esque log here
|
||||||
return gitFiles
|
return files
|
||||||
}
|
}
|
||||||
|
|
||||||
// StashDo modify stash
|
// StashDo modify stash
|
||||||
@ -124,19 +117,19 @@ func (c *GitCommand) StashSave(message string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MergeStatusFiles merge status files
|
// MergeStatusFiles merge status files
|
||||||
func (c *GitCommand) MergeStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitFile {
|
func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []File) []File {
|
||||||
if len(oldGitFiles) == 0 {
|
if len(oldFiles) == 0 {
|
||||||
return newGitFiles
|
return newFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
appendedIndexes := make([]int, 0)
|
appendedIndexes := make([]int, 0)
|
||||||
|
|
||||||
// retain position of files we already could see
|
// retain position of files we already could see
|
||||||
result := make([]GitFile, 0)
|
result := make([]File, 0)
|
||||||
for _, oldGitFile := range oldGitFiles {
|
for _, oldFile := range oldFiles {
|
||||||
for newIndex, newGitFile := range newGitFiles {
|
for newIndex, newFile := range newFiles {
|
||||||
if oldGitFile.Name == newGitFile.Name {
|
if oldFile.Name == newFile.Name {
|
||||||
result = append(result, newGitFile)
|
result = append(result, newFile)
|
||||||
appendedIndexes = append(appendedIndexes, newIndex)
|
appendedIndexes = append(appendedIndexes, newIndex)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -144,9 +137,9 @@ func (c *GitCommand) MergeStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitF
|
|||||||
}
|
}
|
||||||
|
|
||||||
// append any new files to the end
|
// append any new files to the end
|
||||||
for index, newGitFile := range newGitFiles {
|
for index, newFile := range newFiles {
|
||||||
if !includesInt(appendedIndexes, index) {
|
if !includesInt(appendedIndexes, index) {
|
||||||
result = append(result, newGitFile)
|
result = append(result, newFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,17 +210,6 @@ func (c *GitCommand) GetCommitsToPush() []string {
|
|||||||
return utils.SplitLines(pushables)
|
return utils.SplitLines(pushables)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BranchIncluded states whether a branch is included in a list of branches,
|
|
||||||
// with a case insensitive comparison on name
|
|
||||||
func (c *GitCommand) BranchIncluded(branchName string, branches []Branch) bool {
|
|
||||||
for _, existingBranch := range branches {
|
|
||||||
if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenameCommit renames the topmost commit with the given name
|
// RenameCommit renames the topmost commit with the given name
|
||||||
func (c *GitCommand) RenameCommit(name string) (string, error) {
|
func (c *GitCommand) RenameCommit(name string) (string, error) {
|
||||||
return c.OSCommand.RunDirectCommand("git commit --allow-empty --amend -m \"" + name + "\"")
|
return c.OSCommand.RunDirectCommand("git commit --allow-empty --amend -m \"" + name + "\"")
|
||||||
@ -268,25 +250,26 @@ func (c *GitCommand) AbortMerge() (string, error) {
|
|||||||
return c.OSCommand.RunDirectCommand("git merge --abort")
|
return c.OSCommand.RunDirectCommand("git merge --abort")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GitCommit commit to git
|
// Commit commit to git
|
||||||
func (c *GitCommand) GitCommit(g *gocui.Gui, message string) (string, error) {
|
func (c *GitCommand) Commit(g *gocui.Gui, message string) (*exec.Cmd, error) {
|
||||||
command := "git commit -m \"" + message + "\""
|
command := "git commit -m \"" + message + "\""
|
||||||
gpgsign, _ := gitconfig.Global("commit.gpgsign")
|
gpgsign, _ := gitconfig.Global("commit.gpgsign")
|
||||||
if gpgsign != "" {
|
if gpgsign != "" {
|
||||||
sub, err := c.OSCommand.RunSubProcess("git", "commit")
|
return c.OSCommand.PrepareSubProcess("git", "commit")
|
||||||
return "", nil
|
|
||||||
}
|
}
|
||||||
return c.OSCommand.RunDirectCommand(command)
|
// TODO: make these runDirectCommand functions just return an error
|
||||||
|
_, err := c.OSCommand.RunDirectCommand(command)
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GitPull pull from repo
|
// Pull pull from repo
|
||||||
func (c *GitCommand) GitPull() (string, error) {
|
func (c *GitCommand) Pull() (string, error) {
|
||||||
return c.OSCommand.RunCommand("git pull --no-edit")
|
return c.OSCommand.RunCommand("git pull --no-edit")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GitPush push to a branch
|
// Push push to a branch
|
||||||
func (c *GitCommand) GitPush() (string, error) {
|
func (c *GitCommand) Push(branchName string) (string, error) {
|
||||||
return c.OSCommand.RunDirectCommand("git push -u origin " + state.Branches[0].Name)
|
return c.OSCommand.RunDirectCommand("git push -u origin " + branchName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SquashPreviousTwoCommits squashes a commit down to the one below it
|
// SquashPreviousTwoCommits squashes a commit down to the one below it
|
||||||
@ -364,7 +347,7 @@ func (c *GitCommand) IsInMergeState() (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RemoveFile directly
|
// RemoveFile directly
|
||||||
func (c *GitCommand) RemoveFile(file GitFile) error {
|
func (c *GitCommand) RemoveFile(file File) error {
|
||||||
// if the file isn't tracked, we assume you want to delete it
|
// if the file isn't tracked, we assume you want to delete it
|
||||||
if !file.Tracked {
|
if !file.Tracked {
|
||||||
_, err := c.OSCommand.RunCommand("rm -rf ./" + file.Name)
|
_, err := c.OSCommand.RunCommand("rm -rf ./" + file.Name)
|
||||||
@ -384,10 +367,15 @@ func (c *GitCommand) Checkout(branch string, force bool) (string, error) {
|
|||||||
return c.OSCommand.RunCommand("git checkout " + forceArg + branch)
|
return c.OSCommand.RunCommand("git checkout " + forceArg + branch)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddPatch runs a subprocess for adding a patch by patch
|
// AddPatch prepares a subprocess for adding a patch by patch
|
||||||
// this will eventually be swapped out for a better solution inside the Gui
|
// this will eventually be swapped out for a better solution inside the Gui
|
||||||
func (c *GitCommand) AddPatch(g *gocui.Gui, filename string) (*exec.Cmd, error) {
|
func (c *GitCommand) AddPatch(filename string) (*exec.Cmd, error) {
|
||||||
return c.OSCommand.RunSubProcess("git", "add", "--patch", filename)
|
return c.OSCommand.PrepareSubProcess("git", "add", "--patch", filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrepareCommitSubProcess prepares a subprocess for `git commit`
|
||||||
|
func (c *GitCommand) PrepareCommitSubProcess() (*exec.Cmd, error) {
|
||||||
|
return c.OSCommand.PrepareSubProcess("git", "commit")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBranchGraph gets the color-formatted graph of the log for the given branch
|
// GetBranchGraph gets the color-formatted graph of the log for the given branch
|
||||||
@ -428,11 +416,11 @@ func includesInt(list []int, a int) bool {
|
|||||||
|
|
||||||
// GetCommits obtains the commits of the current branch
|
// GetCommits obtains the commits of the current branch
|
||||||
func (c *GitCommand) GetCommits() []Commit {
|
func (c *GitCommand) GetCommits() []Commit {
|
||||||
pushables := gogit.GetCommitsToPush()
|
pushables := c.GetCommitsToPush()
|
||||||
log := getLog()
|
log := c.GetLog()
|
||||||
commits := make([]Commit, 0)
|
commits := make([]Commit, 0)
|
||||||
// now we can split it up and turn it into commits
|
// now we can split it up and turn it into commits
|
||||||
lines := utils.RplitLines(log)
|
lines := utils.SplitLines(log)
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
splitLine := strings.Split(line, " ")
|
splitLine := strings.Split(line, " ")
|
||||||
sha := splitLine[0]
|
sha := splitLine[0]
|
||||||
@ -477,7 +465,7 @@ func (c *GitCommand) Show(sha string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Diff returns the diff of a file
|
// Diff returns the diff of a file
|
||||||
func (c *GitCommand) Diff(file GitFile) string {
|
func (c *GitCommand) Diff(file File) string {
|
||||||
cachedArg := ""
|
cachedArg := ""
|
||||||
if file.HasStagedChanges && !file.HasUnstagedChanges {
|
if file.HasStagedChanges && !file.HasUnstagedChanges {
|
||||||
cachedArg = "--cached "
|
cachedArg = "--cached "
|
||||||
|
@ -2,7 +2,7 @@ package commands
|
|||||||
|
|
||||||
// File : A staged/unstaged file
|
// File : A staged/unstaged file
|
||||||
// TODO: decide whether to give all of these the Git prefix
|
// TODO: decide whether to give all of these the Git prefix
|
||||||
type GitFile struct {
|
type File struct {
|
||||||
Name string
|
Name string
|
||||||
HasStagedChanges bool
|
HasStagedChanges bool
|
||||||
HasUnstagedChanges bool
|
HasUnstagedChanges bool
|
||||||
@ -27,8 +27,10 @@ type StashEntry struct {
|
|||||||
DisplayString string
|
DisplayString string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Branch : A git branch
|
// Conflict : A git conflict with a start middle and end corresponding to line
|
||||||
type Branch struct {
|
// numbers in the file where the conflict bars appear
|
||||||
Name string
|
type Conflict struct {
|
||||||
Recency string
|
start int
|
||||||
|
middle int
|
||||||
|
end int
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,8 @@ import (
|
|||||||
var (
|
var (
|
||||||
// ErrNoOpenCommand : When we don't know which command to use to open a file
|
// ErrNoOpenCommand : When we don't know which command to use to open a file
|
||||||
ErrNoOpenCommand = errors.New("Unsure what command to use to open this file")
|
ErrNoOpenCommand = errors.New("Unsure what command to use to open this file")
|
||||||
|
// ErrNoEditorDefined : When we can't find an editor to edit a file
|
||||||
|
ErrNoEditorDefined = errors.New("No editor defined in $VISUAL, $EDITOR, or git config")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Platform stores the os state
|
// Platform stores the os state
|
||||||
@ -138,14 +140,14 @@ func (c *OSCommand) editFile(g *gocui.Gui, filename string) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if editor == "" {
|
if editor == "" {
|
||||||
return "", createErrorPanel(g, "No editor defined in $VISUAL, $EDITOR, or git config.")
|
return "", ErrNoEditorDefined
|
||||||
}
|
}
|
||||||
c.RunSubProcess(editor, filename)
|
c.PrepareSubProcess(editor, filename)
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunSubProcess iniRunSubProcessrocess then tells the Gui to switch to it
|
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
|
||||||
func (c *OSCommand) RunSubProcess(cmdName string, commandArgs ...string) (*exec.Cmd, error) {
|
func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) (*exec.Cmd, error) {
|
||||||
subprocess := exec.Command(cmdName, commandArgs...)
|
subprocess := exec.Command(cmdName, commandArgs...)
|
||||||
subprocess.Stdin = os.Stdin
|
subprocess.Stdin = os.Stdin
|
||||||
subprocess.Stdout = os.Stdout
|
subprocess.Stdout = os.Stdout
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
|
|
||||||
@ -19,59 +20,61 @@ import (
|
|||||||
// our safe branches, then add the remaining safe branches, ensuring uniqueness
|
// our safe branches, then add the remaining safe branches, ensuring uniqueness
|
||||||
// along the way
|
// along the way
|
||||||
|
|
||||||
|
// BranchListBuilder returns a list of Branch objects for the current repo
|
||||||
type BranchListBuilder struct {
|
type BranchListBuilder struct {
|
||||||
Log *logrus.Log
|
Log *logrus.Logger
|
||||||
GitCommand *commands.GitCommand
|
GitCommand *commands.GitCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBranchListBuilder(log *logrus.Logger, gitCommand *GitCommand) (*BranchListBuilder, error) {
|
// NewBranchListBuilder builds a new branch list builder
|
||||||
return nil, &BranchListBuilder{
|
func NewBranchListBuilder(log *logrus.Logger, gitCommand *commands.GitCommand) (*BranchListBuilder, error) {
|
||||||
Log: log,
|
return &BranchListBuilder{
|
||||||
GitCommand: gitCommand
|
Log: log,
|
||||||
}
|
GitCommand: gitCommand,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *branchListBuilder) ObtainCurrentBranch() Branch {
|
func (b *BranchListBuilder) obtainCurrentBranch() commands.Branch {
|
||||||
// I used go-git for this, but that breaks if you've just done a git init,
|
// I used go-git for this, but that breaks if you've just done a git init,
|
||||||
// even though you're on 'master'
|
// even though you're on 'master'
|
||||||
branchName, _ := runDirectCommand("git symbolic-ref --short HEAD")
|
branchName, _ := b.GitCommand.OSCommand.RunDirectCommand("git symbolic-ref --short HEAD")
|
||||||
return Branch{Name: strings.TrimSpace(branchName), Recency: " *"}
|
return commands.Branch{Name: strings.TrimSpace(branchName), Recency: " *"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*branchListBuilder) ObtainReflogBranches() []Branch {
|
func (b *BranchListBuilder) obtainReflogBranches() []commands.Branch {
|
||||||
branches := make([]Branch, 0)
|
branches := make([]commands.Branch, 0)
|
||||||
rawString, err := runDirectCommand("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
|
rawString, err := b.GitCommand.OSCommand.RunDirectCommand("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return branches
|
return branches
|
||||||
}
|
}
|
||||||
|
|
||||||
branchLines := splitLines(rawString)
|
branchLines := utils.SplitLines(rawString)
|
||||||
for _, line := range branchLines {
|
for _, line := range branchLines {
|
||||||
timeNumber, timeUnit, branchName := branchInfoFromLine(line)
|
timeNumber, timeUnit, branchName := branchInfoFromLine(line)
|
||||||
timeUnit = abbreviatedTimeUnit(timeUnit)
|
timeUnit = abbreviatedTimeUnit(timeUnit)
|
||||||
branch := Branch{Name: branchName, Recency: timeNumber + timeUnit}
|
branch := commands.Branch{Name: branchName, Recency: timeNumber + timeUnit}
|
||||||
branches = append(branches, branch)
|
branches = append(branches, branch)
|
||||||
}
|
}
|
||||||
return branches
|
return branches
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *branchListBuilder) obtainSafeBranches() []Branch {
|
func (b *BranchListBuilder) obtainSafeBranches() []commands.Branch {
|
||||||
branches := make([]Branch, 0)
|
branches := make([]commands.Branch, 0)
|
||||||
|
|
||||||
bIter, err := r.Branches()
|
bIter, err := b.GitCommand.Repo.Branches()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
err = bIter.ForEach(func(b *plumbing.Reference) error {
|
err = bIter.ForEach(func(b *plumbing.Reference) error {
|
||||||
name := b.Name().Short()
|
name := b.Name().Short()
|
||||||
branches = append(branches, Branch{Name: name})
|
branches = append(branches, commands.Branch{Name: name})
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
return branches
|
return branches
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *branchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []Branch, included bool) []Branch {
|
func (b *BranchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []commands.Branch, included bool) []commands.Branch {
|
||||||
for _, newBranch := range newBranches {
|
for _, newBranch := range newBranches {
|
||||||
if included == branchIncluded(newBranch.Name, existingBranches) {
|
if included == branchIncluded(newBranch.Name, existingBranches) {
|
||||||
finalBranches = append(finalBranches, newBranch)
|
finalBranches = append(finalBranches, newBranch)
|
||||||
@ -80,7 +83,7 @@ func (b *branchListBuilder) appendNewBranches(finalBranches, newBranches, existi
|
|||||||
return finalBranches
|
return finalBranches
|
||||||
}
|
}
|
||||||
|
|
||||||
func sanitisedReflogName(reflogBranch Branch, safeBranches []Branch) string {
|
func sanitisedReflogName(reflogBranch commands.Branch, safeBranches []commands.Branch) string {
|
||||||
for _, safeBranch := range safeBranches {
|
for _, safeBranch := range safeBranches {
|
||||||
if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) {
|
if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) {
|
||||||
return safeBranch.Name
|
return safeBranch.Name
|
||||||
@ -89,15 +92,16 @@ func sanitisedReflogName(reflogBranch Branch, safeBranches []Branch) string {
|
|||||||
return reflogBranch.Name
|
return reflogBranch.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *branchListBuilder) build() []Branch {
|
// Build the list of branches for the current repo
|
||||||
branches := make([]Branch, 0)
|
func (b *BranchListBuilder) Build() []commands.Branch {
|
||||||
|
branches := make([]commands.Branch, 0)
|
||||||
head := b.obtainCurrentBranch()
|
head := b.obtainCurrentBranch()
|
||||||
safeBranches := b.obtainSafeBranches()
|
safeBranches := b.obtainSafeBranches()
|
||||||
if len(safeBranches) == 0 {
|
if len(safeBranches) == 0 {
|
||||||
return append(branches, head)
|
return append(branches, head)
|
||||||
}
|
}
|
||||||
reflogBranches := b.obtainReflogBranches()
|
reflogBranches := b.obtainReflogBranches()
|
||||||
reflogBranches = uniqueByName(append([]Branch{head}, reflogBranches...))
|
reflogBranches = uniqueByName(append([]commands.Branch{head}, reflogBranches...))
|
||||||
for i, reflogBranch := range reflogBranches {
|
for i, reflogBranch := range reflogBranches {
|
||||||
reflogBranches[i].Name = sanitisedReflogName(reflogBranch, safeBranches)
|
reflogBranches[i].Name = sanitisedReflogName(reflogBranch, safeBranches)
|
||||||
}
|
}
|
||||||
@ -108,8 +112,17 @@ func (b *branchListBuilder) build() []Branch {
|
|||||||
return branches
|
return branches
|
||||||
}
|
}
|
||||||
|
|
||||||
func uniqueByName(branches []Branch) []Branch {
|
func branchIncluded(branchName string, branches []commands.Branch) bool {
|
||||||
finalBranches := make([]Branch, 0)
|
for _, existingBranch := range branches {
|
||||||
|
if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func uniqueByName(branches []commands.Branch) []commands.Branch {
|
||||||
|
finalBranches := make([]commands.Branch, 0)
|
||||||
for _, branch := range branches {
|
for _, branch := range branches {
|
||||||
if branchIncluded(branch.Name, finalBranches) {
|
if branchIncluded(branch.Name, finalBranches) {
|
||||||
continue
|
continue
|
||||||
|
141
pkg/gui/branches_panel.go
Normal file
141
pkg/gui/branches_panel.go
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/git"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (gui *Gui) handleBranchPress(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
index := gui.getItemPosition(v)
|
||||||
|
if index == 0 {
|
||||||
|
return gui.createErrorPanel(g, "You have already checked out this branch")
|
||||||
|
}
|
||||||
|
branch := gui.getSelectedBranch(v)
|
||||||
|
if output, err := gui.GitCommand.Checkout(branch.Name, false); err != nil {
|
||||||
|
gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
return gui.refreshSidePanels(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
branch := gui.getSelectedBranch(v)
|
||||||
|
return gui.createConfirmationPanel(g, v, "Force Checkout Branch", "Are you sure you want force checkout? You will lose all local changes", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if output, err := gui.GitCommand.Checkout(branch.Name, true); err != nil {
|
||||||
|
gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
return gui.refreshSidePanels(g)
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
gui.createPromptPanel(g, v, "Branch Name:", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if output, err := gui.GitCommand.Checkout(gui.trimmedContent(v), false); err != nil {
|
||||||
|
return gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
return gui.refreshSidePanels(g)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleNewBranch(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
branch := gui.State.Branches[0]
|
||||||
|
gui.createPromptPanel(g, v, "New Branch Name (Branch is off of "+branch.Name+")", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if output, err := gui.GitCommand.NewBranch(gui.trimmedContent(v)); err != nil {
|
||||||
|
return gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
gui.refreshSidePanels(g)
|
||||||
|
return gui.handleBranchSelect(g, v)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleDeleteBranch(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
checkedOutBranch := gui.State.Branches[0]
|
||||||
|
selectedBranch := gui.getSelectedBranch(v)
|
||||||
|
if checkedOutBranch.Name == selectedBranch.Name {
|
||||||
|
return gui.createErrorPanel(g, "You cannot delete the checked out branch!")
|
||||||
|
}
|
||||||
|
return gui.createConfirmationPanel(g, v, "Delete Branch", "Are you sure you want delete the branch "+selectedBranch.Name+" ?", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if output, err := gui.GitCommand.DeleteBranch(selectedBranch.Name); err != nil {
|
||||||
|
return gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
return gui.refreshSidePanels(g)
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleMerge(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
checkedOutBranch := gui.State.Branches[0]
|
||||||
|
selectedBranch := gui.getSelectedBranch(v)
|
||||||
|
defer gui.refreshSidePanels(g)
|
||||||
|
if checkedOutBranch.Name == selectedBranch.Name {
|
||||||
|
return gui.createErrorPanel(g, "You cannot merge a branch into itself")
|
||||||
|
}
|
||||||
|
if output, err := gui.GitCommand.Merge(selectedBranch.Name); err != nil {
|
||||||
|
return gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) getSelectedBranch(v *gocui.View) commands.Branch {
|
||||||
|
lineNumber := gui.getItemPosition(v)
|
||||||
|
return gui.State.Branches[lineNumber]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) renderBranchesOptions(g *gocui.Gui) error {
|
||||||
|
return gui.renderOptionsMap(g, map[string]string{
|
||||||
|
"space": "checkout",
|
||||||
|
"f": "force checkout",
|
||||||
|
"m": "merge",
|
||||||
|
"c": "checkout by name",
|
||||||
|
"n": "new branch",
|
||||||
|
"d": "delete branch",
|
||||||
|
"← → ↑ ↓": "navigate",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// may want to standardise how these select methods work
|
||||||
|
func (gui *Gui) handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if err := gui.renderBranchesOptions(g); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// This really shouldn't happen: there should always be a master branch
|
||||||
|
if len(gui.State.Branches) == 0 {
|
||||||
|
return gui.renderString(g, "main", "No branches for this repo")
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
branch := gui.getSelectedBranch(v)
|
||||||
|
diff, err := gui.GitCommand.GetBranchGraph(branch.Name)
|
||||||
|
if err != nil && strings.HasPrefix(diff, "fatal: ambiguous argument") {
|
||||||
|
diff = "There is no tracking for this branch"
|
||||||
|
}
|
||||||
|
gui.renderString(g, "main", diff)
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshStatus is called at the end of this because that's when we can
|
||||||
|
// be sure there is a state.Branches array to pick the current branch from
|
||||||
|
func (gui *Gui) refreshBranches(g *gocui.Gui) error {
|
||||||
|
g.Update(func(g *gocui.Gui) error {
|
||||||
|
v, err := g.View("branches")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
builder, err := git.NewBranchListBuilder(gui.Log, gui.GitCommand)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gui.State.Branches = builder.Build()
|
||||||
|
v.Clear()
|
||||||
|
for _, branch := range gui.State.Branches {
|
||||||
|
fmt.Fprintln(v, branch.GetDisplayString())
|
||||||
|
}
|
||||||
|
gui.resetOrigin(v)
|
||||||
|
return refreshStatus(g)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
50
pkg/gui/commit_message_panel.go
Normal file
50
pkg/gui/commit_message_panel.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import "github.com/jesseduffield/gocui"
|
||||||
|
|
||||||
|
func (gui *Gui) handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
message := gui.trimmedContent(v)
|
||||||
|
if message == "" {
|
||||||
|
return gui.createErrorPanel(g, "You cannot commit without a commit message")
|
||||||
|
}
|
||||||
|
sub, err := gui.GitCommand.Commit(g, message)
|
||||||
|
if err != nil {
|
||||||
|
// TODO need to find a way to send through this error
|
||||||
|
if err != ErrSubProcess {
|
||||||
|
return gui.createErrorPanel(g, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sub != nil {
|
||||||
|
gui.SubProcess = sub
|
||||||
|
return ErrSubProcess
|
||||||
|
}
|
||||||
|
gui.refreshFiles(g)
|
||||||
|
v.Clear()
|
||||||
|
v.SetCursor(0, 0)
|
||||||
|
g.SetViewOnBottom("commitMessage")
|
||||||
|
gui.switchFocus(g, v, gui.getFilesView(g))
|
||||||
|
return gui.refreshCommits(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleCommitClose(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
g.SetViewOnBottom("commitMessage")
|
||||||
|
return gui.switchFocus(g, v, gui.getFilesView(g))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleNewlineCommitMessage(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
// resising ahead of time so that the top line doesn't get hidden to make
|
||||||
|
// room for the cursor on the second line
|
||||||
|
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, v.Buffer())
|
||||||
|
if _, err := g.SetView("commitMessage", x0, y0, x1, y1+1, 0); err != nil {
|
||||||
|
if err != gocui.ErrUnknownView {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v.EditNewLine()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleCommitFocused(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gui.renderString(g, "options", "esc: close, enter: confirm")
|
||||||
|
}
|
176
pkg/gui/commits_panel.go
Normal file
176
pkg/gui/commits_panel.go
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrNoCommits : When no commits are found for the branch
|
||||||
|
ErrNoCommits = errors.New("No commits for this branch")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (gui *Gui) refreshCommits(g *gocui.Gui) error {
|
||||||
|
g.Update(func(*gocui.Gui) error {
|
||||||
|
gui.State.Commits = gui.GitCommand.GetCommits()
|
||||||
|
v, err := g.View("commits")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
v.Clear()
|
||||||
|
red := color.New(color.FgRed)
|
||||||
|
yellow := color.New(color.FgYellow)
|
||||||
|
white := color.New(color.FgWhite)
|
||||||
|
shaColor := white
|
||||||
|
for _, commit := range gui.State.Commits {
|
||||||
|
if commit.Pushed {
|
||||||
|
shaColor = red
|
||||||
|
} else {
|
||||||
|
shaColor = yellow
|
||||||
|
}
|
||||||
|
shaColor.Fprint(v, commit.Sha+" ")
|
||||||
|
white.Fprintln(v, commit.Name)
|
||||||
|
}
|
||||||
|
refreshStatus(g)
|
||||||
|
if g.CurrentView().Name() == "commits" {
|
||||||
|
gui.handleCommitSelect(g, v)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error {
|
||||||
|
return gui.createConfirmationPanel(g, commitView, "Reset To Commit", "Are you sure you want to reset to this commit?", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
commit, err := gui.getSelectedCommit(g)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if output, err := gui.GitCommand.ResetToCommit(commit.Sha); err != nil {
|
||||||
|
return gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
if err := gui.refreshCommits(g); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err := gui.refreshFiles(g); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
gui.resetOrigin(commitView)
|
||||||
|
return gui.handleCommitSelect(g, nil)
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) renderCommitsOptions(g *gocui.Gui) error {
|
||||||
|
return gui.renderOptionsMap(g, map[string]string{
|
||||||
|
"s": "squash down",
|
||||||
|
"r": "rename",
|
||||||
|
"g": "reset to this commit",
|
||||||
|
"f": "fixup commit",
|
||||||
|
"← → ↑ ↓": "navigate",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if err := gui.renderCommitsOptions(g); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
commit, err := gui.getSelectedCommit(g)
|
||||||
|
if err != nil {
|
||||||
|
if err != ErrNoCommits {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return gui.renderString(g, "main", "No commits for this branch")
|
||||||
|
}
|
||||||
|
commitText := gui.GitCommand.Show(commit.Sha)
|
||||||
|
return gui.renderString(g, "main", commitText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if gui.getItemPosition(v) != 0 {
|
||||||
|
return gui.createErrorPanel(g, "Can only squash topmost commit")
|
||||||
|
}
|
||||||
|
if len(gui.State.Commits) == 1 {
|
||||||
|
return gui.createErrorPanel(g, "You have no commits to squash with")
|
||||||
|
}
|
||||||
|
commit, err := gui.getSelectedCommit(g)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if output, err := gui.GitCommand.SquashPreviousTwoCommits(commit.Name); err != nil {
|
||||||
|
return gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
if err := gui.refreshCommits(g); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
refreshStatus(g)
|
||||||
|
return gui.handleCommitSelect(g, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: move to files panel
|
||||||
|
func (gui *Gui) anyUnStagedChanges(files []commands.File) bool {
|
||||||
|
for _, file := range files {
|
||||||
|
if file.Tracked && file.HasUnstagedChanges {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if len(gui.State.Commits) == 1 {
|
||||||
|
return gui.createErrorPanel(g, "You have no commits to squash with")
|
||||||
|
}
|
||||||
|
if gui.anyUnStagedChanges(gui.State.Files) {
|
||||||
|
return gui.createErrorPanel(g, "Can't fixup while there are unstaged changes")
|
||||||
|
}
|
||||||
|
branch := gui.State.Branches[0]
|
||||||
|
commit, err := gui.getSelectedCommit(g)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gui.createConfirmationPanel(g, v, "Fixup", "Are you sure you want to fixup this commit? The commit beneath will be squashed up into this one", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if output, err := gui.GitCommand.SquashFixupCommit(branch.Name, commit.Sha); err != nil {
|
||||||
|
return gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
if err := gui.refreshCommits(g); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return refreshStatus(g)
|
||||||
|
}, nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if gui.getItemPosition(v) != 0 {
|
||||||
|
return gui.createErrorPanel(g, "Can only rename topmost commit")
|
||||||
|
}
|
||||||
|
gui.createPromptPanel(g, v, "Rename Commit", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if output, err := gui.GitCommand.RenameCommit(v.Buffer()); err != nil {
|
||||||
|
return gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
if err := gui.refreshCommits(g); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return gui.handleCommitSelect(g, v)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) getSelectedCommit(g *gocui.Gui) (commands.Commit, error) {
|
||||||
|
v, err := g.View("commits")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if len(gui.State.Commits) == 0 {
|
||||||
|
return commands.Commit{}, ErrNoCommits
|
||||||
|
}
|
||||||
|
lineNumber := gui.getItemPosition(v)
|
||||||
|
if lineNumber > len(gui.State.Commits)-1 {
|
||||||
|
gui.Log.Info("potential error in getSelected Commit (mismatched ui and state)", gui.State.Commits, lineNumber)
|
||||||
|
return gui.State.Commits[len(gui.State.Commits)-1], nil
|
||||||
|
}
|
||||||
|
return gui.State.Commits[lineNumber], nil
|
||||||
|
}
|
@ -4,39 +4,40 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package panels
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) func(*gocui.Gui, *gocui.View) error {
|
func (gui *Gui) wrappedConfirmationFunction(function func(*gocui.Gui, *gocui.View) error) func(*gocui.Gui, *gocui.View) error {
|
||||||
return func(g *gocui.Gui, v *gocui.View) error {
|
return func(g *gocui.Gui, v *gocui.View) error {
|
||||||
if function != nil {
|
if function != nil {
|
||||||
if err := function(g, v); err != nil {
|
if err := function(g, v); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return closeConfirmationPrompt(g)
|
return gui.closeConfirmationPrompt(g)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func closeConfirmationPrompt(g *gocui.Gui) error {
|
func (gui *Gui) closeConfirmationPrompt(g *gocui.Gui) error {
|
||||||
view, err := g.View("confirmation")
|
view, err := g.View("confirmation")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if err := returnFocus(g, view); err != nil {
|
if err := gui.returnFocus(g, view); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
g.DeleteKeybindings("confirmation")
|
g.DeleteKeybindings("confirmation")
|
||||||
return g.DeleteView("confirmation")
|
return g.DeleteView("confirmation")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMessageHeight(message string, width int) int {
|
func (gui *Gui) getMessageHeight(message string, width int) int {
|
||||||
lines := strings.Split(message, "\n")
|
lines := strings.Split(message, "\n")
|
||||||
lineCount := 0
|
lineCount := 0
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
@ -45,20 +46,20 @@ func getMessageHeight(message string, width int) int {
|
|||||||
return lineCount
|
return lineCount
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConfirmationPanelDimensions(g *gocui.Gui, prompt string) (int, int, int, int) {
|
func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, prompt string) (int, int, int, int) {
|
||||||
width, height := g.Size()
|
width, height := g.Size()
|
||||||
panelWidth := width / 2
|
panelWidth := width / 2
|
||||||
panelHeight := getMessageHeight(prompt, panelWidth)
|
panelHeight := gui.getMessageHeight(prompt, panelWidth)
|
||||||
return width/2 - panelWidth/2,
|
return width/2 - panelWidth/2,
|
||||||
height/2 - panelHeight/2 - panelHeight%2 - 1,
|
height/2 - panelHeight/2 - panelHeight%2 - 1,
|
||||||
width/2 + panelWidth/2,
|
width/2 + panelWidth/2,
|
||||||
height/2 + panelHeight/2
|
height/2 + panelHeight/2
|
||||||
}
|
}
|
||||||
|
|
||||||
func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
|
func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
|
||||||
g.SetViewOnBottom("commitMessage")
|
g.SetViewOnBottom("commitMessage")
|
||||||
// only need to fit one line
|
// only need to fit one line
|
||||||
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, "")
|
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, "")
|
||||||
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil {
|
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil {
|
||||||
if err != gocui.ErrUnknownView {
|
if err != gocui.ErrUnknownView {
|
||||||
return err
|
return err
|
||||||
@ -66,41 +67,41 @@ func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, hand
|
|||||||
|
|
||||||
confirmationView.Editable = true
|
confirmationView.Editable = true
|
||||||
confirmationView.Title = title
|
confirmationView.Title = title
|
||||||
switchFocus(g, currentView, confirmationView)
|
gui.switchFocus(g, currentView, confirmationView)
|
||||||
return setKeyBindings(g, handleConfirm, nil)
|
return gui.setKeyBindings(g, handleConfirm, nil)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
|
func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
|
||||||
g.SetViewOnBottom("commitMessage")
|
g.SetViewOnBottom("commitMessage")
|
||||||
g.Update(func(g *gocui.Gui) error {
|
g.Update(func(g *gocui.Gui) error {
|
||||||
// delete the existing confirmation panel if it exists
|
// delete the existing confirmation panel if it exists
|
||||||
if view, _ := g.View("confirmation"); view != nil {
|
if view, _ := g.View("confirmation"); view != nil {
|
||||||
if err := closeConfirmationPrompt(g); err != nil {
|
if err := gui.closeConfirmationPrompt(g); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, prompt)
|
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, prompt)
|
||||||
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil {
|
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil {
|
||||||
if err != gocui.ErrUnknownView {
|
if err != gocui.ErrUnknownView {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
confirmationView.Title = title
|
confirmationView.Title = title
|
||||||
confirmationView.FgColor = gocui.ColorWhite
|
confirmationView.FgColor = gocui.ColorWhite
|
||||||
renderString(g, "confirmation", prompt)
|
gui.renderString(g, "confirmation", prompt)
|
||||||
switchFocus(g, currentView, confirmationView)
|
gui.switchFocus(g, currentView, confirmationView)
|
||||||
return setKeyBindings(g, handleConfirm, handleClose)
|
return gui.setKeyBindings(g, handleConfirm, handleClose)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleNewline(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) handleNewline(g *gocui.Gui, v *gocui.View) error {
|
||||||
// resising ahead of time so that the top line doesn't get hidden to make
|
// resising ahead of time so that the top line doesn't get hidden to make
|
||||||
// room for the cursor on the second line
|
// room for the cursor on the second line
|
||||||
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, v.Buffer())
|
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, v.Buffer())
|
||||||
if _, err := g.SetView("confirmation", x0, y0, x1, y1+1, 0); err != nil {
|
if _, err := g.SetView("confirmation", x0, y0, x1, y1+1, 0); err != nil {
|
||||||
if err != gocui.ErrUnknownView {
|
if err != gocui.ErrUnknownView {
|
||||||
return err
|
return err
|
||||||
@ -111,45 +112,38 @@ func handleNewline(g *gocui.Gui, v *gocui.View) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
|
func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error {
|
||||||
renderString(g, "options", "esc: close, enter: confirm")
|
gui.renderString(g, "options", "esc: close, enter: confirm")
|
||||||
if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, wrappedConfirmationFunction(handleConfirm)); err != nil {
|
if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := g.SetKeybinding("confirmation", gocui.KeyTab, gocui.ModNone, handleNewline); err != nil {
|
if err := g.SetKeybinding("confirmation", gocui.KeyTab, gocui.ModNone, gui.handleNewline); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return g.SetKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone, wrappedConfirmationFunction(handleClose))
|
return g.SetKeybinding("confirmation", gocui.KeyEsc, gocui.ModNone, gui.wrappedConfirmationFunction(handleClose))
|
||||||
}
|
}
|
||||||
|
|
||||||
func createMessagePanel(g *gocui.Gui, currentView *gocui.View, title, prompt string) error {
|
func (gui *Gui) createMessagePanel(g *gocui.Gui, currentView *gocui.View, title, prompt string) error {
|
||||||
return createConfirmationPanel(g, currentView, title, prompt, nil, nil)
|
return gui.createConfirmationPanel(g, currentView, title, prompt, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createErrorPanel(g *gocui.Gui, message string) error {
|
func (gui *Gui) createErrorPanel(g *gocui.Gui, message string) error {
|
||||||
currentView := g.CurrentView()
|
currentView := g.CurrentView()
|
||||||
colorFunction := color.New(color.FgRed).SprintFunc()
|
colorFunction := color.New(color.FgRed).SprintFunc()
|
||||||
coloredMessage := colorFunction(strings.TrimSpace(message))
|
coloredMessage := colorFunction(strings.TrimSpace(message))
|
||||||
return createConfirmationPanel(g, currentView, "Error", coloredMessage, nil, nil)
|
return gui.createConfirmationPanel(g, currentView, "Error", coloredMessage, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func trimTrailingNewline(str string) string {
|
func (gui *Gui) resizePopupPanel(g *gocui.Gui, v *gocui.View) error {
|
||||||
if strings.HasSuffix(str, "\n") {
|
|
||||||
return str[:len(str)-1]
|
|
||||||
}
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
func resizePopupPanel(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
// If the confirmation panel is already displayed, just resize the width,
|
// If the confirmation panel is already displayed, just resize the width,
|
||||||
// otherwise continue
|
// otherwise continue
|
||||||
content := trimTrailingNewline(v.Buffer())
|
content := utils.TrimTrailingNewline(v.Buffer())
|
||||||
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, content)
|
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, content)
|
||||||
vx0, vy0, vx1, vy1 := v.Dimensions()
|
vx0, vy0, vx1, vy1 := v.Dimensions()
|
||||||
if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 {
|
if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
devLog("resizing popup panel")
|
gui.Log.Info("resizing popup panel")
|
||||||
_, err := g.SetView(v.Name(), x0, y0, x1, y1, 0)
|
_, err := g.SetView(v.Name(), x0, y0, x1, y1, 0)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
383
pkg/gui/files_panel.go
Normal file
383
pkg/gui/files_panel.go
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
package gui
|
||||||
|
|
||||||
|
import (
|
||||||
|
|
||||||
|
// "io"
|
||||||
|
// "io/ioutil"
|
||||||
|
|
||||||
|
// "strings"
|
||||||
|
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errNoFiles = errors.New("No changed files")
|
||||||
|
errNoUsername = errors.New(`No username set. Please do: git config --global user.name "Your Name"`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (gui *Gui) stagedFiles(files []commands.File) []commands.File {
|
||||||
|
result := make([]commands.File, 0)
|
||||||
|
for _, file := range files {
|
||||||
|
if file.HasStagedChanges {
|
||||||
|
result = append(result, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) stageSelectedFile(g *gocui.Gui) error {
|
||||||
|
file, err := gui.getSelectedFile(g)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return gui.GitCommand.StageFile(file.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleFilePress(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
file, err := gui.getSelectedFile(g)
|
||||||
|
if err != nil {
|
||||||
|
if err == errNoFiles {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.HasMergeConflicts {
|
||||||
|
return gui.handleSwitchToMerge(g, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if file.HasUnstagedChanges {
|
||||||
|
gui.GitCommand.StageFile(file.Name)
|
||||||
|
} else {
|
||||||
|
gui.GitCommand.UnStageFile(file.Name, file.Tracked)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gui.refreshFiles(g); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return gui.handleFileSelect(g, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleAddPatch(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
file, err := gui.getSelectedFile(g)
|
||||||
|
if err != nil {
|
||||||
|
if err == errNoFiles {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !file.HasUnstagedChanges {
|
||||||
|
return gui.createErrorPanel(g, "File has no unstaged changes to add")
|
||||||
|
}
|
||||||
|
if !file.Tracked {
|
||||||
|
return gui.createErrorPanel(g, "Cannot git add --patch untracked files")
|
||||||
|
}
|
||||||
|
sub, err := gui.GitCommand.AddPatch(file.Name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gui.SubProcess = sub
|
||||||
|
return ErrSubProcess
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) getSelectedFile(g *gocui.Gui) (commands.File, error) {
|
||||||
|
if len(gui.State.Files) == 0 {
|
||||||
|
return commands.File{}, errNoFiles
|
||||||
|
}
|
||||||
|
filesView, err := g.View("files")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
lineNumber := gui.getItemPosition(filesView)
|
||||||
|
return gui.State.Files[lineNumber], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleFileRemove(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
file, err := gui.getSelectedFile(g)
|
||||||
|
if err != nil {
|
||||||
|
if err == errNoFiles {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var deleteVerb string
|
||||||
|
if file.Tracked {
|
||||||
|
deleteVerb = "checkout"
|
||||||
|
} else {
|
||||||
|
deleteVerb = "delete"
|
||||||
|
}
|
||||||
|
return gui.createConfirmationPanel(g, v, strings.Title(deleteVerb)+" file", "Are you sure you want to "+deleteVerb+" "+file.Name+" (you will lose your changes)?", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if err := gui.GitCommand.RemoveFile(file); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return gui.refreshFiles(g)
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
file, err := gui.getSelectedFile(g)
|
||||||
|
if err != nil {
|
||||||
|
return gui.createErrorPanel(g, err.Error())
|
||||||
|
}
|
||||||
|
if file.Tracked {
|
||||||
|
return gui.createErrorPanel(g, "Cannot ignore tracked files")
|
||||||
|
}
|
||||||
|
gui.GitCommand.Ignore(file.Name)
|
||||||
|
return gui.refreshFiles(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) renderfilesOptions(g *gocui.Gui, file *commands.File) error {
|
||||||
|
optionsMap := map[string]string{
|
||||||
|
"← → ↑ ↓": "navigate",
|
||||||
|
"S": "stash files",
|
||||||
|
"c": "commit changes",
|
||||||
|
"o": "open",
|
||||||
|
"i": "ignore",
|
||||||
|
"d": "delete",
|
||||||
|
"space": "toggle staged",
|
||||||
|
"R": "refresh",
|
||||||
|
"t": "add patch",
|
||||||
|
"e": "edit",
|
||||||
|
"PgUp/PgDn": "scroll",
|
||||||
|
}
|
||||||
|
if gui.State.HasMergeConflicts {
|
||||||
|
optionsMap["a"] = "abort merge"
|
||||||
|
optionsMap["m"] = "resolve merge conflicts"
|
||||||
|
}
|
||||||
|
if file == nil {
|
||||||
|
return gui.renderOptionsMap(g, optionsMap)
|
||||||
|
}
|
||||||
|
if file.Tracked {
|
||||||
|
optionsMap["d"] = "checkout"
|
||||||
|
}
|
||||||
|
return gui.renderOptionsMap(g, optionsMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleFileSelect(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
file, err := gui.getSelectedFile(g)
|
||||||
|
if err != nil {
|
||||||
|
if err != errNoFiles {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gui.renderString(g, "main", "No changed files")
|
||||||
|
return gui.renderfilesOptions(g, nil)
|
||||||
|
}
|
||||||
|
gui.renderfilesOptions(g, &file)
|
||||||
|
var content string
|
||||||
|
if file.HasMergeConflicts {
|
||||||
|
return refreshMergePanel(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
content = gui.GitCommand.Diff(file)
|
||||||
|
return gui.renderString(g, "main", content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
|
||||||
|
if len(gui.stagedFiles(gui.State.Files)) == 0 && !gui.State.HasMergeConflicts {
|
||||||
|
return gui.createErrorPanel(g, "There are no staged files to commit")
|
||||||
|
}
|
||||||
|
commitMessageView := gui.getCommitMessageView(g)
|
||||||
|
g.Update(func(g *gocui.Gui) error {
|
||||||
|
g.SetViewOnTop("commitMessage")
|
||||||
|
gui.switchFocus(g, filesView, commitMessageView)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleCommitEditorPress - handle when the user wants to commit changes via
|
||||||
|
// their editor rather than via the popup panel
|
||||||
|
func (gui *Gui) HandleCommitEditorPress(g *gocui.Gui, filesView *gocui.View) error {
|
||||||
|
if len(gui.stagedFiles(gui.State.Files)) == 0 && !gui.State.HasMergeConflicts {
|
||||||
|
return gui.createErrorPanel(g, "There are no staged files to commit")
|
||||||
|
}
|
||||||
|
gui.PrepareSubProcess(g, "git", "commit")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrepareSubProcess - prepare a subprocess for execution and tell the gui to switch to it
|
||||||
|
func (gui *Gui) PrepareSubProcess(g *gocui.Gui, commands ...string) error {
|
||||||
|
sub, err := gui.GitCommand.PrepareCommitSubProcess()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gui.SubProcess = sub
|
||||||
|
g.Update(func(g *gocui.Gui) error {
|
||||||
|
return ErrSubProcess
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) genericFileOpen(g *gocui.Gui, v *gocui.View, open func(*gocui.Gui, string) (string, error)) error {
|
||||||
|
file, err := gui.getSelectedFile(g)
|
||||||
|
if err != nil {
|
||||||
|
if err != errNoFiles {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := open(g, file.Name); err != nil {
|
||||||
|
return gui.createErrorPanel(g, err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleFileEdit(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gui.genericFileOpen(g, v, gui.editFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gui.genericFileOpen(g, v, gui.openFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gui.genericFileOpen(g, v, gui.sublimeOpenFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleVsCodeFileOpen(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gui.genericFileOpen(g, v, gui.vsCodeOpenFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gui.refreshFiles(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) refreshStateFiles() {
|
||||||
|
// get files to stage
|
||||||
|
files := getGitStatusFiles()
|
||||||
|
gui.State.Files = mergeGitStatusFiles(gui.State.Files, files)
|
||||||
|
updateHasMergeConflictStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) updateHasMergeConflictStatus() error {
|
||||||
|
merging, err := isInMergeState()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gui.State.HasMergeConflicts = merging
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) renderFile(file commands.File, filesView *gocui.View) {
|
||||||
|
// potentially inefficient to be instantiating these color
|
||||||
|
// objects with each render
|
||||||
|
red := color.New(color.FgRed)
|
||||||
|
green := color.New(color.FgGreen)
|
||||||
|
if !file.Tracked && !file.HasStagedChanges {
|
||||||
|
red.Fprintln(filesView, file.DisplayString)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
green.Fprint(filesView, file.DisplayString[0:1])
|
||||||
|
red.Fprint(filesView, file.DisplayString[1:3])
|
||||||
|
if file.HasUnstagedChanges {
|
||||||
|
red.Fprintln(filesView, file.Name)
|
||||||
|
} else {
|
||||||
|
green.Fprintln(filesView, file.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) catSelectedFile(g *gocui.Gui) (string, error) {
|
||||||
|
item, err := gui.getSelectedFile(g)
|
||||||
|
if err != nil {
|
||||||
|
if err != errNoFiles {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return "", gui.renderString(g, "main", "No file to display")
|
||||||
|
}
|
||||||
|
cat, err := catFile(item.Name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return cat, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) refreshFiles(g *gocui.Gui) error {
|
||||||
|
filesView, err := g.View("files")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
refreshStateFiles()
|
||||||
|
filesView.Clear()
|
||||||
|
for _, file := range gui.State.Files {
|
||||||
|
renderFile(file, filesView)
|
||||||
|
}
|
||||||
|
correctCursor(filesView)
|
||||||
|
if filesView == g.CurrentView() {
|
||||||
|
gui.handleFileSelect(g, filesView)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) pullFiles(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
createMessagePanel(g, v, "", "Pulling...")
|
||||||
|
go func() {
|
||||||
|
if output, err := gitPull(); err != nil {
|
||||||
|
gui.createErrorPanel(g, output)
|
||||||
|
} else {
|
||||||
|
gui.closeConfirmationPrompt(g)
|
||||||
|
refreshCommits(g)
|
||||||
|
refreshStatus(g)
|
||||||
|
}
|
||||||
|
gui.refreshFiles(g)
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) pushFiles(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
createMessagePanel(g, v, "", "Pushing...")
|
||||||
|
go func() {
|
||||||
|
branchName = gui.State.Branches[0].Name
|
||||||
|
if output, err := commands.Push(branchName); err != nil {
|
||||||
|
gui.createErrorPanel(g, output)
|
||||||
|
} else {
|
||||||
|
gui.closeConfirmationPrompt(g)
|
||||||
|
refreshCommits(g)
|
||||||
|
refreshStatus(g)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
mergeView, err := g.View("main")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
file, err := gui.getSelectedFile(g)
|
||||||
|
if err != nil {
|
||||||
|
if err != errNoFiles {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !file.HasMergeConflicts {
|
||||||
|
return gui.createErrorPanel(g, "This file has no merge conflicts")
|
||||||
|
}
|
||||||
|
gui.switchFocus(g, v, mergeView)
|
||||||
|
return refreshMergePanel(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
output, err := gitAbortMerge()
|
||||||
|
if err != nil {
|
||||||
|
return gui.createErrorPanel(g, output)
|
||||||
|
}
|
||||||
|
createMessagePanel(g, v, "", "Merge aborted")
|
||||||
|
refreshStatus(g)
|
||||||
|
return gui.refreshFiles(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gui *Gui) handleResetHard(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
return gui.createConfirmationPanel(g, v, "Clear file panel", "Are you sure you want `reset --hard HEAD`? You may lose changes", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
|
if err := commands.ResetHard(); err != nil {
|
||||||
|
gui.createErrorPanel(g, err.Error())
|
||||||
|
}
|
||||||
|
return gui.refreshFiles(g)
|
||||||
|
}, nil)
|
||||||
|
}
|
150
pkg/gui/gui.go
150
pkg/gui/gui.go
@ -14,51 +14,69 @@ import (
|
|||||||
|
|
||||||
// "strings"
|
// "strings"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
"github.com/golang-collections/collections/stack"
|
"github.com/golang-collections/collections/stack"
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
"github.com/jesseduffield/lazygit/pkg/git"
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OverlappingEdges determines if panel edges overlap
|
// OverlappingEdges determines if panel edges overlap
|
||||||
var OverlappingEdges = false
|
var OverlappingEdges = false
|
||||||
|
|
||||||
// ErrSubprocess tells us we're switching to a subprocess so we need to
|
// ErrSubProcess tells us we're switching to a subprocess so we need to
|
||||||
// close the Gui until it is finished
|
// close the Gui until it is finished
|
||||||
var (
|
var (
|
||||||
ErrSubprocess = errors.New("running subprocess")
|
ErrSubProcess = errors.New("running subprocess")
|
||||||
subprocess *exec.Cmd
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type stateType struct {
|
// Gui wraps the gocui Gui object which handles rendering and events
|
||||||
GitFiles []git.File
|
type Gui struct {
|
||||||
Branches []git.Branch
|
Gui *gocui.Gui
|
||||||
Commits []git.Commit
|
Log *logrus.Logger
|
||||||
StashEntries []git.StashEntry
|
GitCommand *commands.GitCommand
|
||||||
|
OSCommand *commands.OSCommand
|
||||||
|
Version string
|
||||||
|
SubProcess *exec.Cmd
|
||||||
|
State StateType
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGui builds a new gui handler
|
||||||
|
func NewGui(log *logrus.Logger, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, version string) (*Gui, error) {
|
||||||
|
initialState := StateType{
|
||||||
|
Files: make([]commands.File, 0),
|
||||||
|
PreviousView: "files",
|
||||||
|
Commits: make([]commands.Commit, 0),
|
||||||
|
StashEntries: make([]commands.StashEntry, 0),
|
||||||
|
ConflictIndex: 0,
|
||||||
|
ConflictTop: true,
|
||||||
|
Conflicts: make([]commands.Conflict, 0),
|
||||||
|
EditHistory: stack.New(),
|
||||||
|
Platform: getPlatform(),
|
||||||
|
Version: "test version", // TODO: send version in
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Gui{
|
||||||
|
Log: log,
|
||||||
|
GitCommand: gitCommand,
|
||||||
|
OSCommand: oSCommand,
|
||||||
|
Version: version,
|
||||||
|
State: initialState,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type StateType struct {
|
||||||
|
Files []commands.File
|
||||||
|
Branches []commands.Branch
|
||||||
|
Commits []commands.Commit
|
||||||
|
StashEntries []commands.StashEntry
|
||||||
PreviousView string
|
PreviousView string
|
||||||
HasMergeConflicts bool
|
HasMergeConflicts bool
|
||||||
ConflictIndex int
|
ConflictIndex int
|
||||||
ConflictTop bool
|
ConflictTop bool
|
||||||
Conflicts []conflict
|
Conflicts []commands.Conflict
|
||||||
EditHistory *stack.Stack
|
EditHistory *stack.Stack
|
||||||
Platform platform
|
Platform platform
|
||||||
}
|
Version string
|
||||||
|
|
||||||
type conflict struct {
|
|
||||||
start int
|
|
||||||
middle int
|
|
||||||
end int
|
|
||||||
}
|
|
||||||
|
|
||||||
var state = stateType{
|
|
||||||
GitFiles: make([]GitFile, 0),
|
|
||||||
PreviousView: "files",
|
|
||||||
Commits: make([]Commit, 0),
|
|
||||||
StashEntries: make([]StashEntry, 0),
|
|
||||||
ConflictIndex: 0,
|
|
||||||
ConflictTop: true,
|
|
||||||
Conflicts: make([]conflict, 0),
|
|
||||||
EditHistory: stack.New(),
|
|
||||||
Platform: getPlatform(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type platform struct {
|
type platform struct {
|
||||||
@ -117,7 +135,7 @@ func max(a, b int) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// layout is called for every screen re-render e.g. when the screen is resized
|
// layout is called for every screen re-render e.g. when the screen is resized
|
||||||
func layout(g *gocui.Gui) error {
|
func (gui *Gui) layout(g *gocui.Gui) error {
|
||||||
g.Highlight = true
|
g.Highlight = true
|
||||||
g.SelFgColor = gocui.ColorWhite | gocui.AttrBold
|
g.SelFgColor = gocui.ColorWhite | gocui.AttrBold
|
||||||
width, height := g.Size()
|
width, height := g.Size()
|
||||||
@ -206,7 +224,7 @@ func layout(g *gocui.Gui) error {
|
|||||||
v.FgColor = gocui.ColorWhite
|
v.FgColor = gocui.ColorWhite
|
||||||
}
|
}
|
||||||
|
|
||||||
if v, err := g.SetView("options", -1, optionsTop, width-len(version)-2, optionsTop+2, 0); err != nil {
|
if v, err := g.SetView("options", -1, optionsTop, width-len(gui.Version)-2, optionsTop+2, 0); err != nil {
|
||||||
if err != gocui.ErrUnknownView {
|
if err != gocui.ErrUnknownView {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -214,7 +232,7 @@ func layout(g *gocui.Gui) error {
|
|||||||
v.Frame = false
|
v.Frame = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if getCommitMessageView(g) == nil {
|
if gui.getCommitMessageView(g) == nil {
|
||||||
// doesn't matter where this view starts because it will be hidden
|
// doesn't matter where this view starts because it will be hidden
|
||||||
if commitMessageView, err := g.SetView("commitMessage", 0, 0, width, height, 0); err != nil {
|
if commitMessageView, err := g.SetView("commitMessage", 0, 0, width, height, 0); err != nil {
|
||||||
if err != gocui.ErrUnknownView {
|
if err != gocui.ErrUnknownView {
|
||||||
@ -227,18 +245,18 @@ func layout(g *gocui.Gui) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if v, err := g.SetView("version", width-len(version)-1, optionsTop, width, optionsTop+2, 0); err != nil {
|
if v, err := g.SetView("version", width-len(gui.Version)-1, optionsTop, width, optionsTop+2, 0); err != nil {
|
||||||
if err != gocui.ErrUnknownView {
|
if err != gocui.ErrUnknownView {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
v.BgColor = gocui.ColorDefault
|
v.BgColor = gocui.ColorDefault
|
||||||
v.FgColor = gocui.ColorGreen
|
v.FgColor = gocui.ColorGreen
|
||||||
v.Frame = false
|
v.Frame = false
|
||||||
renderString(g, "version", version)
|
gui.renderString(g, "version", gui.Version)
|
||||||
|
|
||||||
// these are only called once
|
// these are only called once
|
||||||
handleFileSelect(g, filesView)
|
gui.handleFileSelect(g, filesView)
|
||||||
refreshFiles(g)
|
gui.refreshFiles(g)
|
||||||
refreshBranches(g)
|
refreshBranches(g)
|
||||||
refreshCommits(g)
|
refreshCommits(g)
|
||||||
refreshStashEntries(g)
|
refreshStashEntries(g)
|
||||||
@ -258,10 +276,10 @@ func fetch(g *gocui.Gui) error {
|
|||||||
|
|
||||||
func updateLoader(g *gocui.Gui) error {
|
func updateLoader(g *gocui.Gui) error {
|
||||||
if confirmationView, _ := g.View("confirmation"); confirmationView != nil {
|
if confirmationView, _ := g.View("confirmation"); confirmationView != nil {
|
||||||
content := trimmedContent(confirmationView)
|
content := gui.trimmedContent(confirmationView)
|
||||||
if strings.Contains(content, "...") {
|
if strings.Contains(content, "...") {
|
||||||
staticContent := strings.Split(content, "...")[0] + "..."
|
staticContent := strings.Split(content, "...")[0] + "..."
|
||||||
renderString(g, "confirmation", staticContent+" "+loader())
|
gui.renderString(g, "confirmation", staticContent+" "+loader())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -283,13 +301,40 @@ func resizePopupPanels(g *gocui.Gui) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunWithSubprocesses() {
|
// Run setup the gui with keybindings and start the mainloop
|
||||||
|
func (gui *Gui) Run() (*exec.Cmd, error) {
|
||||||
|
g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer g.Close()
|
||||||
|
|
||||||
|
g.FgColor = gocui.ColorDefault
|
||||||
|
|
||||||
|
goEvery(g, time.Second*60, fetch)
|
||||||
|
goEvery(g, time.Second*10, gui.refreshFiles)
|
||||||
|
goEvery(g, time.Millisecond*10, updateLoader)
|
||||||
|
|
||||||
|
g.SetManagerFunc(gui.layout)
|
||||||
|
|
||||||
|
if err = gui.keybindings(g); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = g.MainLoop()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunWithSubprocesses loops, instantiating a new gocui.Gui with each iteration
|
||||||
|
// if the error returned from a run is a ErrSubProcess, it runs the subprocess
|
||||||
|
// otherwise it handles the error, possibly by quitting the application
|
||||||
|
func (gui *Gui) RunWithSubprocesses() {
|
||||||
for {
|
for {
|
||||||
if err := run(); err != nil {
|
if err := gui.Run(); err != nil {
|
||||||
if err == gocui.ErrQuit {
|
if err == gocui.ErrQuit {
|
||||||
break
|
break
|
||||||
} else if err == ErrSubprocess {
|
} else if err == ErrSubProcess {
|
||||||
subprocess.Run()
|
gui.SubProcess.Run()
|
||||||
} else {
|
} else {
|
||||||
log.Panicln(err)
|
log.Panicln(err)
|
||||||
}
|
}
|
||||||
@ -297,29 +342,6 @@ func RunWithSubprocesses() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func run() (err error) {
|
|
||||||
g, err := gocui.NewGui(gocui.OutputNormal, OverlappingEdges)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer g.Close()
|
|
||||||
|
|
||||||
g.FgColor = gocui.ColorDefault
|
|
||||||
|
|
||||||
goEvery(g, time.Second*60, fetch)
|
|
||||||
goEvery(g, time.Second*10, refreshFiles)
|
|
||||||
goEvery(g, time.Millisecond*10, updateLoader)
|
|
||||||
|
|
||||||
g.SetManagerFunc(layout)
|
|
||||||
|
|
||||||
if err = keybindings(g); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = g.MainLoop()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func quit(g *gocui.Gui, v *gocui.View) error {
|
func quit(g *gocui.Gui, v *gocui.View) error {
|
||||||
return gocui.ErrQuit
|
return gocui.ErrQuit
|
||||||
}
|
}
|
||||||
|
@ -12,58 +12,58 @@ type Binding struct {
|
|||||||
Modifier gocui.Modifier
|
Modifier gocui.Modifier
|
||||||
}
|
}
|
||||||
|
|
||||||
func keybindings(g *gocui.Gui) error {
|
func (gui *Gui) keybindings(g *gocui.Gui) error {
|
||||||
bindings := []Binding{
|
bindings := []Binding{
|
||||||
{ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: quit},
|
{ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: gui.quit},
|
||||||
{ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: quit},
|
{ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: gui.quit},
|
||||||
{ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: scrollUpMain},
|
{ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: gui.scrollUpMain},
|
||||||
{ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: scrollDownMain},
|
{ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: gui.scrollDownMain},
|
||||||
{ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: pushFiles},
|
{ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: gui.pushFiles},
|
||||||
{ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: pullFiles},
|
{ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: gui.pullFiles},
|
||||||
{ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: handleRefresh},
|
{ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: gui.handleRefresh},
|
||||||
{ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: handleCommitPress},
|
{ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: gui.handleCommitPress},
|
||||||
{ViewName: "files", Key: 'C', Modifier: gocui.ModNone, Handler: handleCommitEditorPress},
|
{ViewName: "files", Key: 'C', Modifier: gocui.ModNone, Handler: gui.handleCommitEditorPress},
|
||||||
{ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleFilePress},
|
{ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleFilePress},
|
||||||
{ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: handleFileRemove},
|
{ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleFileRemove},
|
||||||
{ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: handleSwitchToMerge},
|
{ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: gui.handleSwitchToMerge},
|
||||||
{ViewName: "files", Key: 'e', Modifier: gocui.ModNone, Handler: handleFileEdit},
|
{ViewName: "files", Key: 'e', Modifier: gocui.ModNone, Handler: gui.handleFileEdit},
|
||||||
{ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: handleFileOpen},
|
{ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: gui.handleFileOpen},
|
||||||
{ViewName: "files", Key: 's', Modifier: gocui.ModNone, Handler: handleSublimeFileOpen},
|
{ViewName: "files", Key: 's', Modifier: gocui.ModNone, Handler: gui.handleSublimeFileOpen},
|
||||||
{ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: handleVsCodeFileOpen},
|
{ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: gui.handleVsCodeFileOpen},
|
||||||
{ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: handleIgnoreFile},
|
{ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: gui.handleIgnoreFile},
|
||||||
{ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: handleRefreshFiles},
|
{ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: gui.handleRefreshFiles},
|
||||||
{ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: handleStashSave},
|
{ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: gui.handleStashSave},
|
||||||
{ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: handleAbortMerge},
|
{ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: gui.handleAbortMerge},
|
||||||
{ViewName: "files", Key: 't', Modifier: gocui.ModNone, Handler: handleAddPatch},
|
{ViewName: "files", Key: 't', Modifier: gocui.ModNone, Handler: gui.handleAddPatch},
|
||||||
{ViewName: "files", Key: 'D', Modifier: gocui.ModNone, Handler: handleResetHard},
|
{ViewName: "files", Key: 'D', Modifier: gocui.ModNone, Handler: gui.handleResetHard},
|
||||||
{ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleEscapeMerge},
|
{ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.handleEscapeMerge},
|
||||||
{ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handlePickHunk},
|
{ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handlePickHunk},
|
||||||
{ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: handlePickBothHunks},
|
{ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: gui.handlePickBothHunks},
|
||||||
{ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: handleSelectPrevConflict},
|
{ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.handleSelectPrevConflict},
|
||||||
{ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: handleSelectNextConflict},
|
{ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.handleSelectNextConflict},
|
||||||
{ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: handleSelectTop},
|
{ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.handleSelectTop},
|
||||||
{ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: handleSelectBottom},
|
{ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.handleSelectBottom},
|
||||||
{ViewName: "main", Key: 'h', Modifier: gocui.ModNone, Handler: handleSelectPrevConflict},
|
{ViewName: "main", Key: 'h', Modifier: gocui.ModNone, Handler: gui.handleSelectPrevConflict},
|
||||||
{ViewName: "main", Key: 'l', Modifier: gocui.ModNone, Handler: handleSelectNextConflict},
|
{ViewName: "main", Key: 'l', Modifier: gocui.ModNone, Handler: gui.handleSelectNextConflict},
|
||||||
{ViewName: "main", Key: 'k', Modifier: gocui.ModNone, Handler: handleSelectTop},
|
{ViewName: "main", Key: 'k', Modifier: gocui.ModNone, Handler: gui.handleSelectTop},
|
||||||
{ViewName: "main", Key: 'j', Modifier: gocui.ModNone, Handler: handleSelectBottom},
|
{ViewName: "main", Key: 'j', Modifier: gocui.ModNone, Handler: gui.handleSelectBottom},
|
||||||
{ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: handlePopFileSnapshot},
|
{ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: gui.handlePopFileSnapshot},
|
||||||
{ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleBranchPress},
|
{ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleBranchPress},
|
||||||
{ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: handleCheckoutByName},
|
{ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: gui.handleCheckoutByName},
|
||||||
{ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: handleForceCheckout},
|
{ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: gui.handleForceCheckout},
|
||||||
{ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: handleNewBranch},
|
{ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: gui.handleNewBranch},
|
||||||
{ViewName: "branches", Key: 'd', Modifier: gocui.ModNone, Handler: handleDeleteBranch},
|
{ViewName: "branches", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleDeleteBranch},
|
||||||
{ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: handleMerge},
|
{ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: gui.handleMerge},
|
||||||
{ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: handleCommitSquashDown},
|
{ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: gui.handleCommitSquashDown},
|
||||||
{ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: handleRenameCommit},
|
{ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: gui.handleRenameCommit},
|
||||||
{ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: handleResetToCommit},
|
{ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: gui.handleResetToCommit},
|
||||||
{ViewName: "commits", Key: 'f', Modifier: gocui.ModNone, Handler: handleCommitFixup},
|
{ViewName: "commits", Key: 'f', Modifier: gocui.ModNone, Handler: gui.handleCommitFixup},
|
||||||
{ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleStashApply},
|
{ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleStashApply},
|
||||||
{ViewName: "stash", Key: 'g', Modifier: gocui.ModNone, Handler: handleStashPop},
|
{ViewName: "stash", Key: 'g', Modifier: gocui.ModNone, Handler: gui.handleStashPop},
|
||||||
{ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: handleStashDrop},
|
{ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleStashDrop},
|
||||||
{ViewName: "commitMessage", Key: gocui.KeyEnter, Modifier: gocui.ModNone, Handler: handleCommitConfirm},
|
{ViewName: "commitMessage", Key: gocui.KeyEnter, Modifier: gocui.ModNone, Handler: gui.handleCommitConfirm},
|
||||||
{ViewName: "commitMessage", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleCommitClose},
|
{ViewName: "commitMessage", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.handleCommitClose},
|
||||||
{ViewName: "commitMessage", Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: handleNewlineCommitMessage},
|
{ViewName: "commitMessage", Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.handleNewlineCommitMessage},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Would make these keybindings global but that interferes with editing
|
// Would make these keybindings global but that interferes with editing
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// though this panel is called the merge panel, it's really going to use the main panel. This may change in the future
|
// though this panel is called the merge panel, it's really going to use the main panel. This may change in the future
|
||||||
|
|
||||||
package panels
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@ -12,12 +12,14 @@ import (
|
|||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func findConflicts(content string) ([]conflict, error) {
|
func findConflicts(content string) ([]commands.Conflict, error) {
|
||||||
conflicts := make([]conflict, 0)
|
conflicts := make([]commands.Conflict, 0)
|
||||||
var newConflict conflict
|
var newConflict conflict
|
||||||
for i, line := range splitLines(content) {
|
for i, line := range utils.SplitLines(content) {
|
||||||
if line == "<<<<<<< HEAD" || line == "<<<<<<< MERGE_HEAD" || line == "<<<<<<< Updated upstream" {
|
if line == "<<<<<<< HEAD" || line == "<<<<<<< MERGE_HEAD" || line == "<<<<<<< Updated upstream" {
|
||||||
newConflict = conflict{start: i}
|
newConflict = conflict{start: i}
|
||||||
} else if line == "=======" {
|
} else if line == "=======" {
|
||||||
@ -30,15 +32,15 @@ func findConflicts(content string) ([]conflict, error) {
|
|||||||
return conflicts, nil
|
return conflicts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func shiftConflict(conflicts []conflict) (conflict, []conflict) {
|
func shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) {
|
||||||
return conflicts[0], conflicts[1:]
|
return conflicts[0], conflicts[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldHighlightLine(index int, conflict conflict, top bool) bool {
|
func shouldHighlightLine(index int, conflict commands.Conflict, top bool) bool {
|
||||||
return (index >= conflict.start && index <= conflict.middle && top) || (index >= conflict.middle && index <= conflict.end && !top)
|
return (index >= conflict.start && index <= conflict.middle && top) || (index >= conflict.middle && index <= conflict.end && !top)
|
||||||
}
|
}
|
||||||
|
|
||||||
func coloredConflictFile(content string, conflicts []conflict, conflictIndex int, conflictTop, hasFocus bool) (string, error) {
|
func coloredConflictFile(content string, conflicts []commands.Conflict, conflictIndex int, conflictTop, hasFocus bool) (string, error) {
|
||||||
if len(conflicts) == 0 {
|
if len(conflicts) == 0 {
|
||||||
return content, nil
|
return content, nil
|
||||||
}
|
}
|
||||||
@ -87,7 +89,7 @@ func handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error {
|
|||||||
return refreshMergePanel(g)
|
return refreshMergePanel(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isIndexToDelete(i int, conflict conflict, pick string) bool {
|
func isIndexToDelete(i int, conflict commands.Conflict, pick string) bool {
|
||||||
return i == conflict.middle ||
|
return i == conflict.middle ||
|
||||||
i == conflict.start ||
|
i == conflict.start ||
|
||||||
i == conflict.end ||
|
i == conflict.end ||
|
||||||
@ -96,8 +98,8 @@ func isIndexToDelete(i int, conflict conflict, pick string) bool {
|
|||||||
(pick == "top" && i > conflict.middle && i < conflict.end)
|
(pick == "top" && i > conflict.middle && i < conflict.end)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveConflict(g *gocui.Gui, conflict conflict, pick string) error {
|
func resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick string) error {
|
||||||
gitFile, err := getSelectedFile(g)
|
gitFile, err := gui.getSelectedFile(g)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -123,7 +125,7 @@ func resolveConflict(g *gocui.Gui, conflict conflict, pick string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func pushFileSnapshot(g *gocui.Gui) error {
|
func pushFileSnapshot(g *gocui.Gui) error {
|
||||||
gitFile, err := getSelectedFile(g)
|
gitFile, err := gui.getSelectedFile(g)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -140,7 +142,7 @@ func handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
prevContent := state.EditHistory.Pop().(string)
|
prevContent := state.EditHistory.Pop().(string)
|
||||||
gitFile, err := getSelectedFile(g)
|
gitFile, err := gui.getSelectedFile(g)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -204,7 +206,7 @@ func refreshMergePanel(g *gocui.Gui) error {
|
|||||||
if err := scrollToConflict(g); err != nil {
|
if err := scrollToConflict(g); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return renderString(g, "main", content)
|
return gui.renderString(g, "main", content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollToConflict(g *gocui.Gui) error {
|
func scrollToConflict(g *gocui.Gui) error {
|
||||||
@ -234,7 +236,7 @@ func switchToMerging(g *gocui.Gui) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func renderMergeOptions(g *gocui.Gui) error {
|
func renderMergeOptions(g *gocui.Gui) error {
|
||||||
return renderOptionsMap(g, map[string]string{
|
return gui.renderOptionsMap(g, map[string]string{
|
||||||
"↑ ↓": "select hunk",
|
"↑ ↓": "select hunk",
|
||||||
"← →": "navigate conflicts",
|
"← →": "navigate conflicts",
|
||||||
"space": "pick hunk",
|
"space": "pick hunk",
|
||||||
@ -248,8 +250,8 @@ func handleEscapeMerge(g *gocui.Gui, v *gocui.View) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
refreshFiles(g)
|
gui.refreshFiles(g)
|
||||||
return switchFocus(g, v, filesView)
|
return gui.switchFocus(g, v, filesView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCompleteMerge(g *gocui.Gui) error {
|
func handleCompleteMerge(g *gocui.Gui) error {
|
||||||
@ -258,6 +260,6 @@ func handleCompleteMerge(g *gocui.Gui) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
stageSelectedFile(g)
|
stageSelectedFile(g)
|
||||||
refreshFiles(g)
|
gui.refreshFiles(g)
|
||||||
return switchFocus(g, nil, filesView)
|
return gui.switchFocus(g, nil, filesView)
|
||||||
}
|
}
|
@ -1,136 +0,0 @@
|
|||||||
package panels
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/jesseduffield/gocui"
|
|
||||||
)
|
|
||||||
|
|
||||||
func handleBranchPress(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
index := getItemPosition(v)
|
|
||||||
if index == 0 {
|
|
||||||
return createErrorPanel(g, "You have already checked out this branch")
|
|
||||||
}
|
|
||||||
branch := getSelectedBranch(v)
|
|
||||||
if output, err := gitCheckout(branch.Name, false); err != nil {
|
|
||||||
createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
return refreshSidePanels(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleForceCheckout(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
branch := getSelectedBranch(v)
|
|
||||||
return createConfirmationPanel(g, v, "Force Checkout Branch", "Are you sure you want force checkout? You will lose all local changes", func(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if output, err := gitCheckout(branch.Name, true); err != nil {
|
|
||||||
createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
return refreshSidePanels(g)
|
|
||||||
}, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCheckoutByName(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
createPromptPanel(g, v, "Branch Name:", func(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if output, err := gitCheckout(trimmedContent(v), false); err != nil {
|
|
||||||
return createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
return refreshSidePanels(g)
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleNewBranch(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
branch := state.Branches[0]
|
|
||||||
createPromptPanel(g, v, "New Branch Name (Branch is off of "+branch.Name+")", func(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if output, err := gitNewBranch(trimmedContent(v)); err != nil {
|
|
||||||
return createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
refreshSidePanels(g)
|
|
||||||
return handleBranchSelect(g, v)
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleDeleteBranch(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
checkedOutBranch := state.Branches[0]
|
|
||||||
selectedBranch := getSelectedBranch(v)
|
|
||||||
if checkedOutBranch.Name == selectedBranch.Name {
|
|
||||||
return createErrorPanel(g, "You cannot delete the checked out branch!")
|
|
||||||
}
|
|
||||||
return createConfirmationPanel(g, v, "Delete Branch", "Are you sure you want delete the branch "+selectedBranch.Name+" ?", func(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if output, err := gitDeleteBranch(selectedBranch.Name); err != nil {
|
|
||||||
return createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
return refreshSidePanels(g)
|
|
||||||
}, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleMerge(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
checkedOutBranch := state.Branches[0]
|
|
||||||
selectedBranch := getSelectedBranch(v)
|
|
||||||
defer refreshSidePanels(g)
|
|
||||||
if checkedOutBranch.Name == selectedBranch.Name {
|
|
||||||
return createErrorPanel(g, "You cannot merge a branch into itself")
|
|
||||||
}
|
|
||||||
if output, err := gitMerge(selectedBranch.Name); err != nil {
|
|
||||||
return createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSelectedBranch(v *gocui.View) Branch {
|
|
||||||
lineNumber := getItemPosition(v)
|
|
||||||
return state.Branches[lineNumber]
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderBranchesOptions(g *gocui.Gui) error {
|
|
||||||
return renderOptionsMap(g, map[string]string{
|
|
||||||
"space": "checkout",
|
|
||||||
"f": "force checkout",
|
|
||||||
"m": "merge",
|
|
||||||
"c": "checkout by name",
|
|
||||||
"n": "new branch",
|
|
||||||
"d": "delete branch",
|
|
||||||
"← → ↑ ↓": "navigate",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// may want to standardise how these select methods work
|
|
||||||
func handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if err := renderBranchesOptions(g); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// This really shouldn't happen: there should always be a master branch
|
|
||||||
if len(state.Branches) == 0 {
|
|
||||||
return renderString(g, "main", "No branches for this repo")
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
branch := getSelectedBranch(v)
|
|
||||||
diff, err := getBranchGraph(branch.Name)
|
|
||||||
if err != nil && strings.HasPrefix(diff, "fatal: ambiguous argument") {
|
|
||||||
diff = "There is no tracking for this branch"
|
|
||||||
}
|
|
||||||
renderString(g, "main", diff)
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// refreshStatus is called at the end of this because that's when we can
|
|
||||||
// be sure there is a state.Branches array to pick the current branch from
|
|
||||||
func refreshBranches(g *gocui.Gui) error {
|
|
||||||
g.Update(func(g *gocui.Gui) error {
|
|
||||||
v, err := g.View("branches")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
builder := git.newBranchListBuilder() // TODO: add constructor params
|
|
||||||
state.Branches = builder.build()
|
|
||||||
v.Clear()
|
|
||||||
for _, branch := range state.Branches {
|
|
||||||
fmt.Fprintln(v, branch.getDisplayString())
|
|
||||||
}
|
|
||||||
resetOrigin(v)
|
|
||||||
return refreshStatus(g)
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
package panels
|
|
||||||
|
|
||||||
import "github.com/jesseduffield/gocui"
|
|
||||||
|
|
||||||
func handleCommitConfirm(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
message := trimmedContent(v)
|
|
||||||
if message == "" {
|
|
||||||
return createErrorPanel(g, "You cannot commit without a commit message")
|
|
||||||
}
|
|
||||||
if output, err := gitCommit(g, message); err != nil {
|
|
||||||
if err == errNoUsername {
|
|
||||||
return createErrorPanel(g, err.Error())
|
|
||||||
}
|
|
||||||
return createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
refreshFiles(g)
|
|
||||||
v.Clear()
|
|
||||||
v.SetCursor(0, 0)
|
|
||||||
g.SetViewOnBottom("commitMessage")
|
|
||||||
switchFocus(g, v, getFilesView(g))
|
|
||||||
return refreshCommits(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCommitClose(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
g.SetViewOnBottom("commitMessage")
|
|
||||||
return switchFocus(g, v, getFilesView(g))
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleNewlineCommitMessage(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
// resising ahead of time so that the top line doesn't get hidden to make
|
|
||||||
// room for the cursor on the second line
|
|
||||||
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, v.Buffer())
|
|
||||||
if _, err := g.SetView("commitMessage", x0, y0, x1, y1+1, 0); err != nil {
|
|
||||||
if err != gocui.ErrUnknownView {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
v.EditNewLine()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCommitFocused(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
return renderString(g, "options", "esc: close, enter: confirm")
|
|
||||||
}
|
|
@ -1,176 +0,0 @@
|
|||||||
package panels
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/jesseduffield/gocui"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrNoCommits : When no commits are found for the branch
|
|
||||||
ErrNoCommits = errors.New("No commits for this branch")
|
|
||||||
)
|
|
||||||
|
|
||||||
func refreshCommits(g *gocui.Gui) error {
|
|
||||||
g.Update(func(*gocui.Gui) error {
|
|
||||||
state.Commits = getCommits()
|
|
||||||
v, err := g.View("commits")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
v.Clear()
|
|
||||||
red := color.New(color.FgRed)
|
|
||||||
yellow := color.New(color.FgYellow)
|
|
||||||
white := color.New(color.FgWhite)
|
|
||||||
shaColor := white
|
|
||||||
for _, commit := range state.Commits {
|
|
||||||
if commit.Pushed {
|
|
||||||
shaColor = red
|
|
||||||
} else {
|
|
||||||
shaColor = yellow
|
|
||||||
}
|
|
||||||
shaColor.Fprint(v, commit.Sha+" ")
|
|
||||||
white.Fprintln(v, commit.Name)
|
|
||||||
}
|
|
||||||
refreshStatus(g)
|
|
||||||
if g.CurrentView().Name() == "commits" {
|
|
||||||
handleCommitSelect(g, v)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleResetToCommit(g *gocui.Gui, commitView *gocui.View) error {
|
|
||||||
return createConfirmationPanel(g, commitView, "Reset To Commit", "Are you sure you want to reset to this commit?", func(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
commit, err := getSelectedCommit(g)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if output, err := gitResetToCommit(commit.Sha); err != nil {
|
|
||||||
return createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
if err := refreshCommits(g); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if err := refreshFiles(g); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
resetOrigin(commitView)
|
|
||||||
return handleCommitSelect(g, nil)
|
|
||||||
}, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderCommitsOptions(g *gocui.Gui) error {
|
|
||||||
return renderOptionsMap(g, map[string]string{
|
|
||||||
"s": "squash down",
|
|
||||||
"r": "rename",
|
|
||||||
"g": "reset to this commit",
|
|
||||||
"f": "fixup commit",
|
|
||||||
"← → ↑ ↓": "navigate",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCommitSelect(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if err := renderCommitsOptions(g); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
commit, err := getSelectedCommit(g)
|
|
||||||
if err != nil {
|
|
||||||
if err != ErrNoCommits {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return renderString(g, "main", "No commits for this branch")
|
|
||||||
}
|
|
||||||
commitText := gitShow(commit.Sha)
|
|
||||||
return renderString(g, "main", commitText)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCommitSquashDown(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if getItemPosition(v) != 0 {
|
|
||||||
return createErrorPanel(g, "Can only squash topmost commit")
|
|
||||||
}
|
|
||||||
if len(state.Commits) == 1 {
|
|
||||||
return createErrorPanel(g, "You have no commits to squash with")
|
|
||||||
}
|
|
||||||
commit, err := getSelectedCommit(g)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if output, err := gitSquashPreviousTwoCommits(commit.Name); err != nil {
|
|
||||||
return createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
if err := refreshCommits(g); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
refreshStatus(g)
|
|
||||||
return handleCommitSelect(g, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: move to files panel
|
|
||||||
func anyUnStagedChanges(files []GitFile) bool {
|
|
||||||
for _, file := range files {
|
|
||||||
if file.Tracked && file.HasUnstagedChanges {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCommitFixup(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if len(state.Commits) == 1 {
|
|
||||||
return createErrorPanel(g, "You have no commits to squash with")
|
|
||||||
}
|
|
||||||
objectLog(state.GitFiles)
|
|
||||||
if anyUnStagedChanges(state.GitFiles) {
|
|
||||||
return createErrorPanel(g, "Can't fixup while there are unstaged changes")
|
|
||||||
}
|
|
||||||
branch := state.Branches[0]
|
|
||||||
commit, err := getSelectedCommit(g)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
createConfirmationPanel(g, v, "Fixup", "Are you sure you want to fixup this commit? The commit beneath will be squashed up into this one", func(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if output, err := gitSquashFixupCommit(branch.Name, commit.Sha); err != nil {
|
|
||||||
return createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
if err := refreshCommits(g); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return refreshStatus(g)
|
|
||||||
}, nil)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleRenameCommit(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if getItemPosition(v) != 0 {
|
|
||||||
return createErrorPanel(g, "Can only rename topmost commit")
|
|
||||||
}
|
|
||||||
createPromptPanel(g, v, "Rename Commit", func(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if output, err := git.RenameCommit(v.Buffer()); err != nil {
|
|
||||||
return createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
if err := refreshCommits(g); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return handleCommitSelect(g, v)
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSelectedCommit(g *gocui.Gui) (Commit, error) {
|
|
||||||
v, err := g.View("commits")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if len(state.Commits) == 0 {
|
|
||||||
return Commit{}, ErrNoCommits
|
|
||||||
}
|
|
||||||
lineNumber := getItemPosition(v)
|
|
||||||
if lineNumber > len(state.Commits)-1 {
|
|
||||||
devLog("potential error in getSelected Commit (mismatched ui and state)", state.Commits, lineNumber)
|
|
||||||
return state.Commits[len(state.Commits)-1], nil
|
|
||||||
}
|
|
||||||
return state.Commits[lineNumber], nil
|
|
||||||
}
|
|
@ -1,373 +0,0 @@
|
|||||||
package panels
|
|
||||||
|
|
||||||
import (
|
|
||||||
|
|
||||||
// "io"
|
|
||||||
// "io/ioutil"
|
|
||||||
|
|
||||||
// "strings"
|
|
||||||
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/jesseduffield/gocui"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errNoFiles = errors.New("No changed files")
|
|
||||||
errNoUsername = errors.New(`No username set. Please do: git config --global user.name "Your Name"`)
|
|
||||||
)
|
|
||||||
|
|
||||||
func stagedFiles(files []GitFile) []GitFile {
|
|
||||||
result := make([]GitFile, 0)
|
|
||||||
for _, file := range files {
|
|
||||||
if file.HasStagedChanges {
|
|
||||||
result = append(result, file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func stageSelectedFile(g *gocui.Gui) error {
|
|
||||||
file, err := getSelectedFile(g)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return stageFile(file.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleFilePress(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
file, err := getSelectedFile(g)
|
|
||||||
if err != nil {
|
|
||||||
if err == errNoFiles {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if file.HasMergeConflicts {
|
|
||||||
return handleSwitchToMerge(g, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
if file.HasUnstagedChanges {
|
|
||||||
stageFile(file.Name)
|
|
||||||
} else {
|
|
||||||
unStageFile(file.Name, file.Tracked)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := refreshFiles(g); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return handleFileSelect(g, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleAddPatch(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
file, err := getSelectedFile(g)
|
|
||||||
if err != nil {
|
|
||||||
if err == errNoFiles {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !file.HasUnstagedChanges {
|
|
||||||
return createErrorPanel(g, "File has no unstaged changes to add")
|
|
||||||
}
|
|
||||||
if !file.Tracked {
|
|
||||||
return createErrorPanel(g, "Cannot git add --patch untracked files")
|
|
||||||
}
|
|
||||||
gitAddPatch(g, file.Name)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSelectedFile(g *gocui.Gui) (GitFile, error) {
|
|
||||||
if len(state.GitFiles) == 0 {
|
|
||||||
return GitFile{}, errNoFiles
|
|
||||||
}
|
|
||||||
filesView, err := g.View("files")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
lineNumber := getItemPosition(filesView)
|
|
||||||
return state.GitFiles[lineNumber], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleFileRemove(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
file, err := getSelectedFile(g)
|
|
||||||
if err != nil {
|
|
||||||
if err == errNoFiles {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var deleteVerb string
|
|
||||||
if file.Tracked {
|
|
||||||
deleteVerb = "checkout"
|
|
||||||
} else {
|
|
||||||
deleteVerb = "delete"
|
|
||||||
}
|
|
||||||
return createConfirmationPanel(g, v, strings.Title(deleteVerb)+" file", "Are you sure you want to "+deleteVerb+" "+file.Name+" (you will lose your changes)?", func(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if err := removeFile(file); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return refreshFiles(g)
|
|
||||||
}, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleIgnoreFile(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
file, err := getSelectedFile(g)
|
|
||||||
if err != nil {
|
|
||||||
return createErrorPanel(g, err.Error())
|
|
||||||
}
|
|
||||||
if file.Tracked {
|
|
||||||
return createErrorPanel(g, "Cannot ignore tracked files")
|
|
||||||
}
|
|
||||||
gitIgnore(file.Name)
|
|
||||||
return refreshFiles(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderfilesOptions(g *gocui.Gui, gitFile *GitFile) error {
|
|
||||||
optionsMap := map[string]string{
|
|
||||||
"← → ↑ ↓": "navigate",
|
|
||||||
"S": "stash files",
|
|
||||||
"c": "commit changes",
|
|
||||||
"o": "open",
|
|
||||||
"i": "ignore",
|
|
||||||
"d": "delete",
|
|
||||||
"space": "toggle staged",
|
|
||||||
"R": "refresh",
|
|
||||||
"t": "add patch",
|
|
||||||
"e": "edit",
|
|
||||||
"PgUp/PgDn": "scroll",
|
|
||||||
}
|
|
||||||
if state.HasMergeConflicts {
|
|
||||||
optionsMap["a"] = "abort merge"
|
|
||||||
optionsMap["m"] = "resolve merge conflicts"
|
|
||||||
}
|
|
||||||
if gitFile == nil {
|
|
||||||
return renderOptionsMap(g, optionsMap)
|
|
||||||
}
|
|
||||||
if gitFile.Tracked {
|
|
||||||
optionsMap["d"] = "checkout"
|
|
||||||
}
|
|
||||||
return renderOptionsMap(g, optionsMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleFileSelect(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
gitFile, err := getSelectedFile(g)
|
|
||||||
if err != nil {
|
|
||||||
if err != errNoFiles {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
renderString(g, "main", "No changed files")
|
|
||||||
return renderfilesOptions(g, nil)
|
|
||||||
}
|
|
||||||
renderfilesOptions(g, &gitFile)
|
|
||||||
var content string
|
|
||||||
if gitFile.HasMergeConflicts {
|
|
||||||
return refreshMergePanel(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
content = getDiff(gitFile)
|
|
||||||
return renderString(g, "main", content)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCommitPress(g *gocui.Gui, filesView *gocui.View) error {
|
|
||||||
if len(stagedFiles(state.GitFiles)) == 0 && !state.HasMergeConflicts {
|
|
||||||
return createErrorPanel(g, "There are no staged files to commit")
|
|
||||||
}
|
|
||||||
commitMessageView := getCommitMessageView(g)
|
|
||||||
g.Update(func(g *gocui.Gui) error {
|
|
||||||
g.SetViewOnTop("commitMessage")
|
|
||||||
switchFocus(g, filesView, commitMessageView)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCommitEditorPress(g *gocui.Gui, filesView *gocui.View) error {
|
|
||||||
if len(stagedFiles(state.GitFiles)) == 0 && !state.HasMergeConflicts {
|
|
||||||
return createErrorPanel(g, "There are no staged files to commit")
|
|
||||||
}
|
|
||||||
runSubProcess(g, "git", "commit")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func runSubprocess(g *gocui.Gui, commands string...) error {
|
|
||||||
var err error
|
|
||||||
// need this OsCommand to be available
|
|
||||||
if subprocess, err = osCommand.RunSubProcess(commands...); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
g.Update(func(g *gocui.Gui) error {
|
|
||||||
return gui.ErrSubprocess
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func genericFileOpen(g *gocui.Gui, v *gocui.View, open func(*gocui.Gui, string) (string, error)) error {
|
|
||||||
file, err := getSelectedFile(g)
|
|
||||||
if err != nil {
|
|
||||||
if err != errNoFiles {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if _, err := open(g, file.Name); err != nil {
|
|
||||||
return createErrorPanel(g, err.Error())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleFileEdit(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
return genericFileOpen(g, v, editFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleFileOpen(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
return genericFileOpen(g, v, openFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
return genericFileOpen(g, v, sublimeOpenFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleVsCodeFileOpen(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
return genericFileOpen(g, v, vsCodeOpenFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleRefreshFiles(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
return refreshFiles(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
func refreshStateGitFiles() {
|
|
||||||
// get files to stage
|
|
||||||
gitFiles := getGitStatusFiles()
|
|
||||||
state.GitFiles = mergeGitStatusFiles(state.GitFiles, gitFiles)
|
|
||||||
updateHasMergeConflictStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateHasMergeConflictStatus() error {
|
|
||||||
merging, err := isInMergeState()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
state.HasMergeConflicts = merging
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderGitFile(gitFile GitFile, filesView *gocui.View) {
|
|
||||||
// potentially inefficient to be instantiating these color
|
|
||||||
// objects with each render
|
|
||||||
red := color.New(color.FgRed)
|
|
||||||
green := color.New(color.FgGreen)
|
|
||||||
if !gitFile.Tracked && !gitFile.HasStagedChanges {
|
|
||||||
red.Fprintln(filesView, gitFile.DisplayString)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
green.Fprint(filesView, gitFile.DisplayString[0:1])
|
|
||||||
red.Fprint(filesView, gitFile.DisplayString[1:3])
|
|
||||||
if gitFile.HasUnstagedChanges {
|
|
||||||
red.Fprintln(filesView, gitFile.Name)
|
|
||||||
} else {
|
|
||||||
green.Fprintln(filesView, gitFile.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func catSelectedFile(g *gocui.Gui) (string, error) {
|
|
||||||
item, err := getSelectedFile(g)
|
|
||||||
if err != nil {
|
|
||||||
if err != errNoFiles {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return "", renderString(g, "main", "No file to display")
|
|
||||||
}
|
|
||||||
cat, err := catFile(item.Name)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return cat, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func refreshFiles(g *gocui.Gui) error {
|
|
||||||
filesView, err := g.View("files")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
refreshStateGitFiles()
|
|
||||||
filesView.Clear()
|
|
||||||
for _, gitFile := range state.GitFiles {
|
|
||||||
renderGitFile(gitFile, filesView)
|
|
||||||
}
|
|
||||||
correctCursor(filesView)
|
|
||||||
if filesView == g.CurrentView() {
|
|
||||||
handleFileSelect(g, filesView)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func pullFiles(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
createMessagePanel(g, v, "", "Pulling...")
|
|
||||||
go func() {
|
|
||||||
if output, err := gitPull(); err != nil {
|
|
||||||
createErrorPanel(g, output)
|
|
||||||
} else {
|
|
||||||
closeConfirmationPrompt(g)
|
|
||||||
refreshCommits(g)
|
|
||||||
refreshStatus(g)
|
|
||||||
}
|
|
||||||
refreshFiles(g)
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func pushFiles(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
createMessagePanel(g, v, "", "Pushing...")
|
|
||||||
go func() {
|
|
||||||
if output, err := gitPush(); err != nil {
|
|
||||||
createErrorPanel(g, output)
|
|
||||||
} else {
|
|
||||||
closeConfirmationPrompt(g)
|
|
||||||
refreshCommits(g)
|
|
||||||
refreshStatus(g)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
mergeView, err := g.View("main")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
file, err := getSelectedFile(g)
|
|
||||||
if err != nil {
|
|
||||||
if err != errNoFiles {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if !file.HasMergeConflicts {
|
|
||||||
return createErrorPanel(g, "This file has no merge conflicts")
|
|
||||||
}
|
|
||||||
switchFocus(g, v, mergeView)
|
|
||||||
return refreshMergePanel(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
output, err := gitAbortMerge()
|
|
||||||
if err != nil {
|
|
||||||
return createErrorPanel(g, output)
|
|
||||||
}
|
|
||||||
createMessagePanel(g, v, "", "Merge aborted")
|
|
||||||
refreshStatus(g)
|
|
||||||
return refreshFiles(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleResetHard(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
return createConfirmationPanel(g, v, "Clear file panel", "Are you sure you want `reset --hard HEAD`? You may lose changes", func(g *gocui.Gui, v *gocui.View) error {
|
|
||||||
if err := git.ResetHard(); err != nil {
|
|
||||||
createErrorPanel(g, err.Error())
|
|
||||||
}
|
|
||||||
return refreshFiles(g)
|
|
||||||
}, nil)
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
package panels
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -17,7 +17,7 @@ func refreshStashEntries(g *gocui.Gui) error {
|
|||||||
for _, stashEntry := range state.StashEntries {
|
for _, stashEntry := range state.StashEntries {
|
||||||
fmt.Fprintln(v, stashEntry.DisplayString)
|
fmt.Fprintln(v, stashEntry.DisplayString)
|
||||||
}
|
}
|
||||||
return resetOrigin(v)
|
return gui.resetOrigin(v)
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -26,12 +26,12 @@ func getSelectedStashEntry(v *gocui.View) *StashEntry {
|
|||||||
if len(state.StashEntries) == 0 {
|
if len(state.StashEntries) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
lineNumber := getItemPosition(v)
|
lineNumber := gui.getItemPosition(v)
|
||||||
return &state.StashEntries[lineNumber]
|
return &state.StashEntries[lineNumber]
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderStashOptions(g *gocui.Gui) error {
|
func renderStashOptions(g *gocui.Gui) error {
|
||||||
return renderOptionsMap(g, map[string]string{
|
return gui.renderOptionsMap(g, map[string]string{
|
||||||
"space": "apply",
|
"space": "apply",
|
||||||
"g": "pop",
|
"g": "pop",
|
||||||
"d": "drop",
|
"d": "drop",
|
||||||
@ -46,11 +46,11 @@ func handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error {
|
|||||||
go func() {
|
go func() {
|
||||||
stashEntry := getSelectedStashEntry(v)
|
stashEntry := getSelectedStashEntry(v)
|
||||||
if stashEntry == nil {
|
if stashEntry == nil {
|
||||||
renderString(g, "main", "No stash entries")
|
gui.renderString(g, "main", "No stash entries")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
diff, _ := getStashEntryDiff(stashEntry.Index)
|
diff, _ := getStashEntryDiff(stashEntry.Index)
|
||||||
renderString(g, "main", diff)
|
gui.renderString(g, "main", diff)
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -64,7 +64,7 @@ func handleStashPop(g *gocui.Gui, v *gocui.View) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleStashDrop(g *gocui.Gui, v *gocui.View) error {
|
func handleStashDrop(g *gocui.Gui, v *gocui.View) error {
|
||||||
return createConfirmationPanel(g, v, "Stash drop", "Are you sure you want to drop this stash entry?", func(g *gocui.Gui, v *gocui.View) error {
|
return gui.createConfirmationPanel(g, v, "Stash drop", "Are you sure you want to drop this stash entry?", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
return stashDo(g, v, "drop")
|
return stashDo(g, v, "drop")
|
||||||
}, nil)
|
}, nil)
|
||||||
}
|
}
|
||||||
@ -72,22 +72,22 @@ func handleStashDrop(g *gocui.Gui, v *gocui.View) error {
|
|||||||
func stashDo(g *gocui.Gui, v *gocui.View, method string) error {
|
func stashDo(g *gocui.Gui, v *gocui.View, method string) error {
|
||||||
stashEntry := getSelectedStashEntry(v)
|
stashEntry := getSelectedStashEntry(v)
|
||||||
if stashEntry == nil {
|
if stashEntry == nil {
|
||||||
return createErrorPanel(g, "No stash to "+method)
|
return gui.createErrorPanel(g, "No stash to "+method)
|
||||||
}
|
}
|
||||||
if output, err := gitStashDo(stashEntry.Index, method); err != nil {
|
if output, err := gitStashDo(stashEntry.Index, method); err != nil {
|
||||||
createErrorPanel(g, output)
|
gui.createErrorPanel(g, output)
|
||||||
}
|
}
|
||||||
refreshStashEntries(g)
|
refreshStashEntries(g)
|
||||||
return refreshFiles(g)
|
return gui.refreshFiles(g)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleStashSave(g *gocui.Gui, filesView *gocui.View) error {
|
func handleStashSave(g *gocui.Gui, filesView *gocui.View) error {
|
||||||
createPromptPanel(g, filesView, "Stash changes", func(g *gocui.Gui, v *gocui.View) error {
|
gui.createPromptPanel(g, filesView, "Stash changes", func(g *gocui.Gui, v *gocui.View) error {
|
||||||
if output, err := gitStashSave(trimmedContent(v)); err != nil {
|
if output, err := gitStashSave(gui.trimmedContent(v)); err != nil {
|
||||||
createErrorPanel(g, output)
|
gui.createErrorPanel(g, output)
|
||||||
}
|
}
|
||||||
refreshStashEntries(g)
|
refreshStashEntries(g)
|
||||||
return refreshFiles(g)
|
return gui.refreshFiles(g)
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package panels
|
package gui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
@ -11,14 +11,14 @@ import (
|
|||||||
|
|
||||||
var cyclableViews = []string{"files", "branches", "commits", "stash"}
|
var cyclableViews = []string{"files", "branches", "commits", "stash"}
|
||||||
|
|
||||||
func refreshSidePanels(g *gocui.Gui) error {
|
func (gui *Gui) refreshSidePanels(g *gocui.Gui) error {
|
||||||
refreshBranches(g)
|
gui.refreshBranches(g)
|
||||||
refreshFiles(g)
|
gui.gui.refreshFiles(g)
|
||||||
refreshCommits(g)
|
gui.refreshCommits(g)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func nextView(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) nextView(g *gocui.Gui, v *gocui.View) error {
|
||||||
var focusedViewName string
|
var focusedViewName string
|
||||||
if v == nil || v.Name() == cyclableViews[len(cyclableViews)-1] {
|
if v == nil || v.Name() == cyclableViews[len(cyclableViews)-1] {
|
||||||
focusedViewName = cyclableViews[0]
|
focusedViewName = cyclableViews[0]
|
||||||
@ -29,7 +29,7 @@ func nextView(g *gocui.Gui, v *gocui.View) error {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
if i == len(cyclableViews)-1 {
|
if i == len(cyclableViews)-1 {
|
||||||
devLog(v.Name() + " is not in the list of views")
|
gui.Log.Info(v.Name() + " is not in the list of views")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -38,10 +38,10 @@ func nextView(g *gocui.Gui, v *gocui.View) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return switchFocus(g, v, focusedView)
|
return gui.gui.switchFocus(g, v, focusedView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func previousView(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) previousView(g *gocui.Gui, v *gocui.View) error {
|
||||||
var focusedViewName string
|
var focusedViewName string
|
||||||
if v == nil || v.Name() == cyclableViews[0] {
|
if v == nil || v.Name() == cyclableViews[0] {
|
||||||
focusedViewName = cyclableViews[len(cyclableViews)-1]
|
focusedViewName = cyclableViews[len(cyclableViews)-1]
|
||||||
@ -61,16 +61,16 @@ func previousView(g *gocui.Gui, v *gocui.View) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return switchFocus(g, v, focusedView)
|
return gui.switchFocus(g, v, focusedView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newLineFocused(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error {
|
||||||
mainView, _ := g.View("main")
|
mainView, _ := g.View("main")
|
||||||
mainView.SetOrigin(0, 0)
|
mainView.SetOrigin(0, 0)
|
||||||
|
|
||||||
switch v.Name() {
|
switch v.Name() {
|
||||||
case "files":
|
case "files":
|
||||||
return handleFileSelect(g, v)
|
return gui.handleFileSelect(g, v)
|
||||||
case "branches":
|
case "branches":
|
||||||
return handleBranchSelect(g, v)
|
return handleBranchSelect(g, v)
|
||||||
case "confirmation":
|
case "confirmation":
|
||||||
@ -91,16 +91,16 @@ func newLineFocused(g *gocui.Gui, v *gocui.View) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func returnFocus(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) returnFocus(g *gocui.Gui, v *gocui.View) error {
|
||||||
previousView, err := g.View(state.PreviousView)
|
previousView, err := g.View(state.PreviousView)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return switchFocus(g, v, previousView)
|
return gui.switchFocus(g, v, previousView)
|
||||||
}
|
}
|
||||||
|
|
||||||
// pass in oldView = nil if you don't want to be able to return to your old view
|
// pass in oldView = nil if you don't want to be able to return to your old view
|
||||||
func switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
|
func (gui *Gui) switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
|
||||||
// we assume we'll never want to return focus to a confirmation panel i.e.
|
// we assume we'll never want to return focus to a confirmation panel i.e.
|
||||||
// we should never stack confirmation panels
|
// we should never stack confirmation panels
|
||||||
if oldView != nil && oldView.Name() != "confirmation" {
|
if oldView != nil && oldView.Name() != "confirmation" {
|
||||||
@ -117,13 +117,13 @@ func switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error {
|
|||||||
return newLineFocused(g, newView)
|
return newLineFocused(g, newView)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getItemPosition(v *gocui.View) int {
|
func (gui *Gui) getItemPosition(v *gocui.View) int {
|
||||||
_, cy := v.Cursor()
|
_, cy := v.Cursor()
|
||||||
_, oy := v.Origin()
|
_, oy := v.Origin()
|
||||||
return oy + cy
|
return oy + cy
|
||||||
}
|
}
|
||||||
|
|
||||||
func cursorUp(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) cursorUp(g *gocui.Gui, v *gocui.View) error {
|
||||||
// swallowing cursor movements in main
|
// swallowing cursor movements in main
|
||||||
// TODO: pull this out
|
// TODO: pull this out
|
||||||
if v == nil || v.Name() == "main" {
|
if v == nil || v.Name() == "main" {
|
||||||
@ -142,7 +142,7 @@ func cursorUp(g *gocui.Gui, v *gocui.View) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cursorDown(g *gocui.Gui, v *gocui.View) error {
|
func (gui *Gui) cursorDown(g *gocui.Gui, v *gocui.View) error {
|
||||||
// swallowing cursor movements in main
|
// swallowing cursor movements in main
|
||||||
// TODO: pull this out
|
// TODO: pull this out
|
||||||
if v == nil || v.Name() == "main" {
|
if v == nil || v.Name() == "main" {
|
||||||
@ -163,15 +163,15 @@ func cursorDown(g *gocui.Gui, v *gocui.View) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetOrigin(v *gocui.View) error {
|
func (gui *Gui) resetOrigin(v *gocui.View) error {
|
||||||
if err := v.SetCursor(0, 0); err != nil {
|
if err := v.SetCursor(0, 0); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return v.SetOrigin(0, 0)
|
return v.SetOrigin(0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the cursor down past the last item, move it up one
|
// if the cursor down past the last item, move it to the last line
|
||||||
func correctCursor(v *gocui.View) error {
|
func (gui *Gui) correctCursor(v *gocui.View) error {
|
||||||
cx, cy := v.Cursor()
|
cx, cy := v.Cursor()
|
||||||
_, oy := v.Origin()
|
_, oy := v.Origin()
|
||||||
lineCount := len(v.BufferLines()) - 2
|
lineCount := len(v.BufferLines()) - 2
|
||||||
@ -181,7 +181,7 @@ func correctCursor(v *gocui.View) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderString(g *gocui.Gui, viewName, s string) error {
|
func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) error {
|
||||||
g.Update(func(*gocui.Gui) error {
|
g.Update(func(*gocui.Gui) error {
|
||||||
v, err := g.View(viewName)
|
v, err := g.View(viewName)
|
||||||
// just in case the view disappeared as this function was called, we'll
|
// just in case the view disappeared as this function was called, we'll
|
||||||
@ -197,7 +197,7 @@ func renderString(g *gocui.Gui, viewName, s string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func optionsMapToString(optionsMap map[string]string) string {
|
func (gui *Gui) optionsMapToString(optionsMap map[string]string) string {
|
||||||
optionsArray := make([]string, 0)
|
optionsArray := make([]string, 0)
|
||||||
for key, description := range optionsMap {
|
for key, description := range optionsMap {
|
||||||
optionsArray = append(optionsArray, key+": "+description)
|
optionsArray = append(optionsArray, key+": "+description)
|
||||||
@ -206,11 +206,11 @@ func optionsMapToString(optionsMap map[string]string) string {
|
|||||||
return strings.Join(optionsArray, ", ")
|
return strings.Join(optionsArray, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) error {
|
func (gui *Gui) renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) error {
|
||||||
return renderString(g, "options", optionsMapToString(optionsMap))
|
return gui.renderString(g, "options", optionsMapToString(optionsMap))
|
||||||
}
|
}
|
||||||
|
|
||||||
func loader() string {
|
func (gui *Gui) loader() string {
|
||||||
characters := "|/-\\"
|
characters := "|/-\\"
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
nanos := now.UnixNano()
|
nanos := now.UnixNano()
|
||||||
@ -219,21 +219,21 @@ func loader() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: refactor properly
|
// TODO: refactor properly
|
||||||
func getFilesView(g *gocui.Gui) *gocui.View {
|
func (gui *Gui) getFilesView(g *gocui.Gui) *gocui.View {
|
||||||
v, _ := g.View("files")
|
v, _ := g.View("files")
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCommitsView(g *gocui.Gui) *gocui.View {
|
func (gui *Gui) getCommitsView(g *gocui.Gui) *gocui.View {
|
||||||
v, _ := g.View("commits")
|
v, _ := g.View("commits")
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCommitMessageView(g *gocui.Gui) *gocui.View {
|
func (gui *Gui) getCommitMessageView(g *gocui.Gui) *gocui.View {
|
||||||
v, _ := g.View("commitMessage")
|
v, _ := g.View("commitMessage")
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func trimmedContent(v *gocui.View) string {
|
func (gui *Gui) trimmedContent(v *gocui.View) string {
|
||||||
return strings.TrimSpace(v.Buffer())
|
return strings.TrimSpace(v.Buffer())
|
||||||
}
|
}
|
||||||
|
@ -54,3 +54,12 @@ func GetCurrentProject() string {
|
|||||||
}
|
}
|
||||||
return filepath.Base(pwd)
|
return filepath.Base(pwd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TrimTrailingNewline - Trims the trailing newline
|
||||||
|
// TODO: replace with `chomp` after refactor
|
||||||
|
func TrimTrailingNewline(str string) string {
|
||||||
|
if strings.HasSuffix(str, "\n") {
|
||||||
|
return str[:len(str)-1]
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
49
utils.go
49
utils.go
@ -1,49 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
|
||||||
)
|
|
||||||
|
|
||||||
func splitLines(multilineString string) []string {
|
|
||||||
multilineString = strings.Replace(multilineString, "\r", "", -1)
|
|
||||||
if multilineString == "" || multilineString == "\n" {
|
|
||||||
return make([]string, 0)
|
|
||||||
}
|
|
||||||
lines := strings.Split(multilineString, "\n")
|
|
||||||
if lines[len(lines)-1] == "" {
|
|
||||||
return lines[:len(lines)-1]
|
|
||||||
}
|
|
||||||
return lines
|
|
||||||
}
|
|
||||||
|
|
||||||
func withPadding(str string, padding int) string {
|
|
||||||
if padding-len(str) < 0 {
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
return str + strings.Repeat(" ", padding-len(str))
|
|
||||||
}
|
|
||||||
|
|
||||||
func coloredString(str string, colorAttribute color.Attribute) string {
|
|
||||||
colour := color.New(colorAttribute)
|
|
||||||
return coloredStringDirect(str, colour)
|
|
||||||
}
|
|
||||||
|
|
||||||
// used for aggregating a few color attributes rather than just sending a single one
|
|
||||||
func coloredStringDirect(str string, colour *color.Color) string {
|
|
||||||
return colour.SprintFunc()(fmt.Sprint(str))
|
|
||||||
}
|
|
||||||
|
|
||||||
// used to get the project name
|
|
||||||
func getCurrentProject() string {
|
|
||||||
pwd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err.Error())
|
|
||||||
}
|
|
||||||
return filepath.Base(pwd)
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user