1
0
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:
Jesse Duffield 2018-08-13 20:26:02 +10:00
parent f9c39ad64b
commit 97cff65612
23 changed files with 1104 additions and 1086 deletions

View File

@ -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
} }

View File

@ -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 {

View File

@ -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 "

View File

@ -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
} }

View File

@ -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

View File

@ -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
View 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
}

View 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
View 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
}

View File

@ -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
View 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)
}

View File

@ -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
} }

View File

@ -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

View File

@ -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)
} }

View File

@ -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
}

View File

@ -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")
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
} }

View File

@ -1,4 +1,4 @@
package panels package gui
import ( import (
"fmt" "fmt"

View File

@ -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())
} }

View File

@ -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
}

View File

@ -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)
}