From 97cff656121270e9c790432e28622d92ab7b0f1a Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 13 Aug 2018 20:26:02 +1000 Subject: [PATCH] progress on refactor --- pkg/app/app.go | 7 + pkg/{git => commands}/branch.go | 16 +- pkg/commands/git.go | 92 +++-- pkg/commands/git_structs.go | 12 +- pkg/commands/os.go | 10 +- pkg/git/branch_list_builder.go | 63 ++-- pkg/gui/branches_panel.go | 141 ++++++++ pkg/gui/commit_message_panel.go | 50 +++ pkg/gui/commits_panel.go | 176 ++++++++++ pkg/gui/{panels => }/confirmation_panel.go | 74 ++-- pkg/gui/files_panel.go | 383 +++++++++++++++++++++ pkg/gui/gui.go | 150 ++++---- pkg/gui/keybindings.go | 102 +++--- pkg/gui/{panels => }/merge_panel.go | 38 +- pkg/gui/panels/branches_panel.go | 136 -------- pkg/gui/panels/commit_message_panel.go | 45 --- pkg/gui/panels/commits_panel.go | 176 ---------- pkg/gui/panels/files_panel.go | 373 -------------------- pkg/gui/{panels => }/stash_panel.go | 28 +- pkg/gui/{panels => }/status_panel.go | 2 +- pkg/gui/view_helpers.go | 58 ++-- pkg/utils/utils.go | 9 + utils.go | 49 --- 23 files changed, 1104 insertions(+), 1086 deletions(-) rename pkg/{git => commands}/branch.go (61%) create mode 100644 pkg/gui/branches_panel.go create mode 100644 pkg/gui/commit_message_panel.go create mode 100644 pkg/gui/commits_panel.go rename pkg/gui/{panels => }/confirmation_panel.go (53%) create mode 100644 pkg/gui/files_panel.go rename pkg/gui/{panels => }/merge_panel.go (84%) delete mode 100644 pkg/gui/panels/branches_panel.go delete mode 100644 pkg/gui/panels/commit_message_panel.go delete mode 100644 pkg/gui/panels/commits_panel.go delete mode 100644 pkg/gui/panels/files_panel.go rename pkg/gui/{panels => }/stash_panel.go (69%) rename pkg/gui/{panels => }/status_panel.go (98%) delete mode 100644 utils.go diff --git a/pkg/app/app.go b/pkg/app/app.go index 726567b01..b6318b745 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -4,8 +4,10 @@ import ( "io" "github.com/Sirupsen/logrus" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/gui" ) // App struct @@ -16,6 +18,7 @@ type App struct { Log *logrus.Logger OSCommand *commands.OSCommand GitCommand *commands.GitCommand + Gui *gocui.Gui } // NewApp retruns a new applications @@ -34,6 +37,10 @@ func NewApp(config config.AppConfigurer) (*App, error) { if err != nil { return nil, err } + app.Gui, err = gui.NewGui(app.Log, app.GitCommand, config.GetVersion()) + if err != nil { + return nil, err + } return app, nil } diff --git a/pkg/git/branch.go b/pkg/commands/branch.go similarity index 61% rename from pkg/git/branch.go rename to pkg/commands/branch.go index 78a52bbc8..13c26e766 100644 --- a/pkg/git/branch.go +++ b/pkg/commands/branch.go @@ -1,15 +1,23 @@ -package git +package commands import ( "strings" "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 -// func (b *Branch) GetDisplayString() string { -// return gui.withPadding(b.Recency, 4) + gui.coloredString(b.Name, b.getColor()) -// } +func (b *Branch) GetDisplayString() string { + return utils.WithPadding(b.Recency, 4) + utils.ColoredString(b.Name, b.GetColor()) +} // GetColor branch color func (b *Branch) GetColor() color.Attribute { diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 99ecec720..016a08fc6 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -38,13 +38,6 @@ func (c *GitCommand) SetupGit() { 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 func (c *GitCommand) GetStashEntries() []StashEntry { stashEntries := make([]StashEntry, 0) @@ -78,10 +71,10 @@ func includes(array []string, str string) bool { } // GetStatusFiles git status files -func (c *GitCommand) GetStatusFiles() []GitFile { +func (c *GitCommand) GetStatusFiles() []File { statusOutput, _ := c.GitStatus() statusStrings := utils.SplitLines(statusOutput) - gitFiles := make([]GitFile, 0) + files := make([]File, 0) for _, statusString := range statusStrings { change := statusString[0:2] @@ -89,7 +82,7 @@ func (c *GitCommand) GetStatusFiles() []GitFile { unstagedChange := statusString[1:2] filename := statusString[3:] tracked := !includes([]string{"??", "A "}, change) - gitFile := GitFile{ + file := File{ Name: filename, DisplayString: statusString, HasStagedChanges: !includes([]string{" ", "U", "?"}, stagedChange), @@ -98,10 +91,10 @@ func (c *GitCommand) GetStatusFiles() []GitFile { Deleted: unstagedChange == "D" || stagedChange == "D", HasMergeConflicts: change == "UU", } - gitFiles = append(gitFiles, gitFile) + files = append(files, file) } - c.Log.Info(gitFiles) // TODO: use a dumper-esque log here - return gitFiles + c.Log.Info(files) // TODO: use a dumper-esque log here + return files } // StashDo modify stash @@ -124,19 +117,19 @@ func (c *GitCommand) StashSave(message string) (string, error) { } // MergeStatusFiles merge status files -func (c *GitCommand) MergeStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitFile { - if len(oldGitFiles) == 0 { - return newGitFiles +func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []File) []File { + if len(oldFiles) == 0 { + return newFiles } appendedIndexes := make([]int, 0) // retain position of files we already could see - result := make([]GitFile, 0) - for _, oldGitFile := range oldGitFiles { - for newIndex, newGitFile := range newGitFiles { - if oldGitFile.Name == newGitFile.Name { - result = append(result, newGitFile) + result := make([]File, 0) + for _, oldFile := range oldFiles { + for newIndex, newFile := range newFiles { + if oldFile.Name == newFile.Name { + result = append(result, newFile) appendedIndexes = append(appendedIndexes, newIndex) break } @@ -144,9 +137,9 @@ func (c *GitCommand) MergeStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitF } // append any new files to the end - for index, newGitFile := range newGitFiles { + for index, newFile := range newFiles { 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) } -// 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 func (c *GitCommand) RenameCommit(name string) (string, error) { 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") } -// GitCommit commit to git -func (c *GitCommand) GitCommit(g *gocui.Gui, message string) (string, error) { +// Commit commit to git +func (c *GitCommand) Commit(g *gocui.Gui, message string) (*exec.Cmd, error) { command := "git commit -m \"" + message + "\"" gpgsign, _ := gitconfig.Global("commit.gpgsign") if gpgsign != "" { - sub, err := c.OSCommand.RunSubProcess("git", "commit") - return "", nil + return c.OSCommand.PrepareSubProcess("git", "commit") } - 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 -func (c *GitCommand) GitPull() (string, error) { +// Pull pull from repo +func (c *GitCommand) Pull() (string, error) { return c.OSCommand.RunCommand("git pull --no-edit") } -// GitPush push to a branch -func (c *GitCommand) GitPush() (string, error) { - return c.OSCommand.RunDirectCommand("git push -u origin " + state.Branches[0].Name) +// Push push to a branch +func (c *GitCommand) Push(branchName string) (string, error) { + return c.OSCommand.RunDirectCommand("git push -u origin " + branchName) } // SquashPreviousTwoCommits squashes a commit down to the one below it @@ -364,7 +347,7 @@ func (c *GitCommand) IsInMergeState() (bool, error) { } // 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 !file.Tracked { _, 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) } -// 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 -func (c *GitCommand) AddPatch(g *gocui.Gui, filename string) (*exec.Cmd, error) { - return c.OSCommand.RunSubProcess("git", "add", "--patch", filename) +func (c *GitCommand) AddPatch(filename string) (*exec.Cmd, error) { + 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 @@ -428,11 +416,11 @@ func includesInt(list []int, a int) bool { // GetCommits obtains the commits of the current branch func (c *GitCommand) GetCommits() []Commit { - pushables := gogit.GetCommitsToPush() - log := getLog() + pushables := c.GetCommitsToPush() + log := c.GetLog() commits := make([]Commit, 0) // now we can split it up and turn it into commits - lines := utils.RplitLines(log) + lines := utils.SplitLines(log) for _, line := range lines { splitLine := strings.Split(line, " ") sha := splitLine[0] @@ -477,7 +465,7 @@ func (c *GitCommand) Show(sha string) string { } // Diff returns the diff of a file -func (c *GitCommand) Diff(file GitFile) string { +func (c *GitCommand) Diff(file File) string { cachedArg := "" if file.HasStagedChanges && !file.HasUnstagedChanges { cachedArg = "--cached " diff --git a/pkg/commands/git_structs.go b/pkg/commands/git_structs.go index dd28d15fa..2f7255be1 100644 --- a/pkg/commands/git_structs.go +++ b/pkg/commands/git_structs.go @@ -2,7 +2,7 @@ package commands // File : A staged/unstaged file // TODO: decide whether to give all of these the Git prefix -type GitFile struct { +type File struct { Name string HasStagedChanges bool HasUnstagedChanges bool @@ -27,8 +27,10 @@ type StashEntry struct { DisplayString string } -// Branch : A git branch -type Branch struct { - Name string - Recency string +// Conflict : A git conflict with a start middle and end corresponding to line +// numbers in the file where the conflict bars appear +type Conflict struct { + start int + middle int + end int } diff --git a/pkg/commands/os.go b/pkg/commands/os.go index e7fe4515f..2313b5550 100644 --- a/pkg/commands/os.go +++ b/pkg/commands/os.go @@ -15,6 +15,8 @@ import ( var ( // 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") + // 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 @@ -138,14 +140,14 @@ func (c *OSCommand) editFile(g *gocui.Gui, filename string) (string, error) { } } 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 } -// RunSubProcess iniRunSubProcessrocess then tells the Gui to switch to it -func (c *OSCommand) RunSubProcess(cmdName string, commandArgs ...string) (*exec.Cmd, error) { +// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it +func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) (*exec.Cmd, error) { subprocess := exec.Command(cmdName, commandArgs...) subprocess.Stdin = os.Stdin subprocess.Stdout = os.Stdout diff --git a/pkg/git/branch_list_builder.go b/pkg/git/branch_list_builder.go index 2f80dba32..faa073119 100644 --- a/pkg/git/branch_list_builder.go +++ b/pkg/git/branch_list_builder.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/Sirupsen/logrus" @@ -19,59 +20,61 @@ import ( // our safe branches, then add the remaining safe branches, ensuring uniqueness // along the way +// BranchListBuilder returns a list of Branch objects for the current repo type BranchListBuilder struct { - Log *logrus.Log + Log *logrus.Logger GitCommand *commands.GitCommand } -func NewBranchListBuilder(log *logrus.Logger, gitCommand *GitCommand) (*BranchListBuilder, error) { - return nil, &BranchListBuilder{ - Log: log, - GitCommand: gitCommand - } +// NewBranchListBuilder builds a new branch list builder +func NewBranchListBuilder(log *logrus.Logger, gitCommand *commands.GitCommand) (*BranchListBuilder, error) { + return &BranchListBuilder{ + 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, // even though you're on 'master' - branchName, _ := runDirectCommand("git symbolic-ref --short HEAD") - return Branch{Name: strings.TrimSpace(branchName), Recency: " *"} + branchName, _ := b.GitCommand.OSCommand.RunDirectCommand("git symbolic-ref --short HEAD") + return commands.Branch{Name: strings.TrimSpace(branchName), Recency: " *"} } -func (*branchListBuilder) ObtainReflogBranches() []Branch { - branches := make([]Branch, 0) - rawString, err := runDirectCommand("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD") +func (b *BranchListBuilder) obtainReflogBranches() []commands.Branch { + branches := make([]commands.Branch, 0) + rawString, err := b.GitCommand.OSCommand.RunDirectCommand("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD") if err != nil { return branches } - branchLines := splitLines(rawString) + branchLines := utils.SplitLines(rawString) for _, line := range branchLines { timeNumber, timeUnit, branchName := branchInfoFromLine(line) timeUnit = abbreviatedTimeUnit(timeUnit) - branch := Branch{Name: branchName, Recency: timeNumber + timeUnit} + branch := commands.Branch{Name: branchName, Recency: timeNumber + timeUnit} branches = append(branches, branch) } return branches } -func (b *branchListBuilder) obtainSafeBranches() []Branch { - branches := make([]Branch, 0) +func (b *BranchListBuilder) obtainSafeBranches() []commands.Branch { + branches := make([]commands.Branch, 0) - bIter, err := r.Branches() + bIter, err := b.GitCommand.Repo.Branches() if err != nil { panic(err) } err = bIter.ForEach(func(b *plumbing.Reference) error { name := b.Name().Short() - branches = append(branches, Branch{Name: name}) + branches = append(branches, commands.Branch{Name: name}) return nil }) 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 { if included == branchIncluded(newBranch.Name, existingBranches) { finalBranches = append(finalBranches, newBranch) @@ -80,7 +83,7 @@ func (b *branchListBuilder) appendNewBranches(finalBranches, newBranches, existi return finalBranches } -func sanitisedReflogName(reflogBranch Branch, safeBranches []Branch) string { +func sanitisedReflogName(reflogBranch commands.Branch, safeBranches []commands.Branch) string { for _, safeBranch := range safeBranches { if strings.ToLower(safeBranch.Name) == strings.ToLower(reflogBranch.Name) { return safeBranch.Name @@ -89,15 +92,16 @@ func sanitisedReflogName(reflogBranch Branch, safeBranches []Branch) string { return reflogBranch.Name } -func (b *branchListBuilder) build() []Branch { - branches := make([]Branch, 0) +// Build the list of branches for the current repo +func (b *BranchListBuilder) Build() []commands.Branch { + branches := make([]commands.Branch, 0) head := b.obtainCurrentBranch() safeBranches := b.obtainSafeBranches() if len(safeBranches) == 0 { return append(branches, head) } reflogBranches := b.obtainReflogBranches() - reflogBranches = uniqueByName(append([]Branch{head}, reflogBranches...)) + reflogBranches = uniqueByName(append([]commands.Branch{head}, reflogBranches...)) for i, reflogBranch := range reflogBranches { reflogBranches[i].Name = sanitisedReflogName(reflogBranch, safeBranches) } @@ -108,8 +112,17 @@ func (b *branchListBuilder) build() []Branch { return branches } -func uniqueByName(branches []Branch) []Branch { - finalBranches := make([]Branch, 0) +func branchIncluded(branchName string, branches []commands.Branch) bool { + 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 { if branchIncluded(branch.Name, finalBranches) { continue diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go new file mode 100644 index 000000000..53b8465cb --- /dev/null +++ b/pkg/gui/branches_panel.go @@ -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 +} diff --git a/pkg/gui/commit_message_panel.go b/pkg/gui/commit_message_panel.go new file mode 100644 index 000000000..f765ab308 --- /dev/null +++ b/pkg/gui/commit_message_panel.go @@ -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") +} diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go new file mode 100644 index 000000000..45134e44a --- /dev/null +++ b/pkg/gui/commits_panel.go @@ -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 +} diff --git a/pkg/gui/panels/confirmation_panel.go b/pkg/gui/confirmation_panel.go similarity index 53% rename from pkg/gui/panels/confirmation_panel.go rename to pkg/gui/confirmation_panel.go index 776504e66..3bec18419 100644 --- a/pkg/gui/panels/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -4,39 +4,40 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package panels +package gui import ( "strings" "github.com/fatih/color" "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 { if function != nil { if err := function(g, v); err != nil { 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") if err != nil { panic(err) } - if err := returnFocus(g, view); err != nil { + if err := gui.returnFocus(g, view); err != nil { panic(err) } g.DeleteKeybindings("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") lineCount := 0 for _, line := range lines { @@ -45,20 +46,20 @@ func getMessageHeight(message string, width int) int { 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() panelWidth := width / 2 - panelHeight := getMessageHeight(prompt, panelWidth) + panelHeight := gui.getMessageHeight(prompt, panelWidth) return width/2 - panelWidth/2, height/2 - panelHeight/2 - panelHeight%2 - 1, width/2 + panelWidth/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") // 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 err != gocui.ErrUnknownView { return err @@ -66,41 +67,41 @@ func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, hand confirmationView.Editable = true confirmationView.Title = title - switchFocus(g, currentView, confirmationView) - return setKeyBindings(g, handleConfirm, nil) + gui.switchFocus(g, currentView, confirmationView) + return gui.setKeyBindings(g, handleConfirm, 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.Update(func(g *gocui.Gui) error { // delete the existing confirmation panel if it exists if view, _ := g.View("confirmation"); view != nil { - if err := closeConfirmationPrompt(g); err != nil { + if err := gui.closeConfirmationPrompt(g); err != nil { 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 err != gocui.ErrUnknownView { return err } confirmationView.Title = title confirmationView.FgColor = gocui.ColorWhite - renderString(g, "confirmation", prompt) - switchFocus(g, currentView, confirmationView) - return setKeyBindings(g, handleConfirm, handleClose) + gui.renderString(g, "confirmation", prompt) + gui.switchFocus(g, currentView, confirmationView) + return gui.setKeyBindings(g, handleConfirm, handleClose) } 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 // 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 != gocui.ErrUnknownView { return err @@ -111,45 +112,38 @@ func handleNewline(g *gocui.Gui, v *gocui.View) error { return nil } -func setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { - renderString(g, "options", "esc: close, enter: confirm") - if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, wrappedConfirmationFunction(handleConfirm)); err != nil { +func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { + gui.renderString(g, "options", "esc: close, enter: confirm") + if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm)); err != nil { 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 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 { - return createConfirmationPanel(g, currentView, title, prompt, nil, nil) +func (gui *Gui) createMessagePanel(g *gocui.Gui, currentView *gocui.View, title, prompt string) error { + 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() colorFunction := color.New(color.FgRed).SprintFunc() 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 { - if strings.HasSuffix(str, "\n") { - return str[:len(str)-1] - } - return str -} - -func resizePopupPanel(g *gocui.Gui, v *gocui.View) error { +func (gui *Gui) resizePopupPanel(g *gocui.Gui, v *gocui.View) error { // If the confirmation panel is already displayed, just resize the width, // otherwise continue - content := trimTrailingNewline(v.Buffer()) - x0, y0, x1, y1 := getConfirmationPanelDimensions(g, content) + content := utils.TrimTrailingNewline(v.Buffer()) + x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, content) vx0, vy0, vx1, vy1 := v.Dimensions() if vx0 == x0 && vy0 == y0 && vx1 == x1 && vy1 == y1 { return nil } - devLog("resizing popup panel") + gui.Log.Info("resizing popup panel") _, err := g.SetView(v.Name(), x0, y0, x1, y1, 0) return err } diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go new file mode 100644 index 000000000..9ffcafa44 --- /dev/null +++ b/pkg/gui/files_panel.go @@ -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) +} diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 8c455d369..402b4ff32 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -14,51 +14,69 @@ import ( // "strings" + "github.com/Sirupsen/logrus" "github.com/golang-collections/collections/stack" "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/git" + "github.com/jesseduffield/lazygit/pkg/commands" ) // OverlappingEdges determines if panel edges overlap 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 var ( - ErrSubprocess = errors.New("running subprocess") - subprocess *exec.Cmd + ErrSubProcess = errors.New("running subprocess") ) -type stateType struct { - GitFiles []git.File - Branches []git.Branch - Commits []git.Commit - StashEntries []git.StashEntry +// Gui wraps the gocui Gui object which handles rendering and events +type Gui struct { + Gui *gocui.Gui + Log *logrus.Logger + 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 HasMergeConflicts bool ConflictIndex int ConflictTop bool - Conflicts []conflict + Conflicts []commands.Conflict EditHistory *stack.Stack Platform platform -} - -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(), + Version string } 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 -func layout(g *gocui.Gui) error { +func (gui *Gui) layout(g *gocui.Gui) error { g.Highlight = true g.SelFgColor = gocui.ColorWhite | gocui.AttrBold width, height := g.Size() @@ -206,7 +224,7 @@ func layout(g *gocui.Gui) error { 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 { return err } @@ -214,7 +232,7 @@ func layout(g *gocui.Gui) error { v.Frame = false } - if getCommitMessageView(g) == nil { + if gui.getCommitMessageView(g) == nil { // 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 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 { return err } v.BgColor = gocui.ColorDefault v.FgColor = gocui.ColorGreen v.Frame = false - renderString(g, "version", version) + gui.renderString(g, "version", gui.Version) // these are only called once - handleFileSelect(g, filesView) - refreshFiles(g) + gui.handleFileSelect(g, filesView) + gui.refreshFiles(g) refreshBranches(g) refreshCommits(g) refreshStashEntries(g) @@ -258,10 +276,10 @@ func fetch(g *gocui.Gui) error { func updateLoader(g *gocui.Gui) error { if confirmationView, _ := g.View("confirmation"); confirmationView != nil { - content := trimmedContent(confirmationView) + content := gui.trimmedContent(confirmationView) if strings.Contains(content, "...") { staticContent := strings.Split(content, "...")[0] + "..." - renderString(g, "confirmation", staticContent+" "+loader()) + gui.renderString(g, "confirmation", staticContent+" "+loader()) } } return nil @@ -283,13 +301,40 @@ func resizePopupPanels(g *gocui.Gui) error { 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 { - if err := run(); err != nil { + if err := gui.Run(); err != nil { if err == gocui.ErrQuit { break - } else if err == ErrSubprocess { - subprocess.Run() + } else if err == ErrSubProcess { + gui.SubProcess.Run() } else { 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 { return gocui.ErrQuit } diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 23f320ceb..194746d28 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -12,58 +12,58 @@ type Binding struct { Modifier gocui.Modifier } -func keybindings(g *gocui.Gui) error { +func (gui *Gui) keybindings(g *gocui.Gui) error { bindings := []Binding{ - {ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: quit}, - {ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: quit}, - {ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: scrollUpMain}, - {ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: scrollDownMain}, - {ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: pushFiles}, - {ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: pullFiles}, - {ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: handleRefresh}, - {ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: handleCommitPress}, - {ViewName: "files", Key: 'C', Modifier: gocui.ModNone, Handler: handleCommitEditorPress}, - {ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleFilePress}, - {ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: handleFileRemove}, - {ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: handleSwitchToMerge}, - {ViewName: "files", Key: 'e', Modifier: gocui.ModNone, Handler: handleFileEdit}, - {ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: handleFileOpen}, - {ViewName: "files", Key: 's', Modifier: gocui.ModNone, Handler: handleSublimeFileOpen}, - {ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: handleVsCodeFileOpen}, - {ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: handleIgnoreFile}, - {ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: handleRefreshFiles}, - {ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: handleStashSave}, - {ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: handleAbortMerge}, - {ViewName: "files", Key: 't', Modifier: gocui.ModNone, Handler: handleAddPatch}, - {ViewName: "files", Key: 'D', Modifier: gocui.ModNone, Handler: handleResetHard}, - {ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleEscapeMerge}, - {ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handlePickHunk}, - {ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: handlePickBothHunks}, - {ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: handleSelectPrevConflict}, - {ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: handleSelectNextConflict}, - {ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: handleSelectTop}, - {ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: handleSelectBottom}, - {ViewName: "main", Key: 'h', Modifier: gocui.ModNone, Handler: handleSelectPrevConflict}, - {ViewName: "main", Key: 'l', Modifier: gocui.ModNone, Handler: handleSelectNextConflict}, - {ViewName: "main", Key: 'k', Modifier: gocui.ModNone, Handler: handleSelectTop}, - {ViewName: "main", Key: 'j', Modifier: gocui.ModNone, Handler: handleSelectBottom}, - {ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: handlePopFileSnapshot}, - {ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleBranchPress}, - {ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: handleCheckoutByName}, - {ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: handleForceCheckout}, - {ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: handleNewBranch}, - {ViewName: "branches", Key: 'd', Modifier: gocui.ModNone, Handler: handleDeleteBranch}, - {ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: handleMerge}, - {ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: handleCommitSquashDown}, - {ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: handleRenameCommit}, - {ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: handleResetToCommit}, - {ViewName: "commits", Key: 'f', Modifier: gocui.ModNone, Handler: handleCommitFixup}, - {ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleStashApply}, - {ViewName: "stash", Key: 'g', Modifier: gocui.ModNone, Handler: handleStashPop}, - {ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: handleStashDrop}, - {ViewName: "commitMessage", Key: gocui.KeyEnter, Modifier: gocui.ModNone, Handler: handleCommitConfirm}, - {ViewName: "commitMessage", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleCommitClose}, - {ViewName: "commitMessage", Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: handleNewlineCommitMessage}, + {ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: gui.quit}, + {ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: gui.quit}, + {ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: gui.scrollUpMain}, + {ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: gui.scrollDownMain}, + {ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: gui.pushFiles}, + {ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: gui.pullFiles}, + {ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: gui.handleRefresh}, + {ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: gui.handleCommitPress}, + {ViewName: "files", Key: 'C', Modifier: gocui.ModNone, Handler: gui.handleCommitEditorPress}, + {ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleFilePress}, + {ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleFileRemove}, + {ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: gui.handleSwitchToMerge}, + {ViewName: "files", Key: 'e', Modifier: gocui.ModNone, Handler: gui.handleFileEdit}, + {ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: gui.handleFileOpen}, + {ViewName: "files", Key: 's', Modifier: gocui.ModNone, Handler: gui.handleSublimeFileOpen}, + {ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: gui.handleVsCodeFileOpen}, + {ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: gui.handleIgnoreFile}, + {ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: gui.handleRefreshFiles}, + {ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: gui.handleStashSave}, + {ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: gui.handleAbortMerge}, + {ViewName: "files", Key: 't', Modifier: gocui.ModNone, Handler: gui.handleAddPatch}, + {ViewName: "files", Key: 'D', Modifier: gocui.ModNone, Handler: gui.handleResetHard}, + {ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.handleEscapeMerge}, + {ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handlePickHunk}, + {ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: gui.handlePickBothHunks}, + {ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: gui.handleSelectPrevConflict}, + {ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: gui.handleSelectNextConflict}, + {ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: gui.handleSelectTop}, + {ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: gui.handleSelectBottom}, + {ViewName: "main", Key: 'h', Modifier: gocui.ModNone, Handler: gui.handleSelectPrevConflict}, + {ViewName: "main", Key: 'l', Modifier: gocui.ModNone, Handler: gui.handleSelectNextConflict}, + {ViewName: "main", Key: 'k', Modifier: gocui.ModNone, Handler: gui.handleSelectTop}, + {ViewName: "main", Key: 'j', Modifier: gocui.ModNone, Handler: gui.handleSelectBottom}, + {ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: gui.handlePopFileSnapshot}, + {ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleBranchPress}, + {ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: gui.handleCheckoutByName}, + {ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: gui.handleForceCheckout}, + {ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: gui.handleNewBranch}, + {ViewName: "branches", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleDeleteBranch}, + {ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: gui.handleMerge}, + {ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: gui.handleCommitSquashDown}, + {ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: gui.handleRenameCommit}, + {ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: gui.handleResetToCommit}, + {ViewName: "commits", Key: 'f', Modifier: gocui.ModNone, Handler: gui.handleCommitFixup}, + {ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleStashApply}, + {ViewName: "stash", Key: 'g', Modifier: gocui.ModNone, Handler: gui.handleStashPop}, + {ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: gui.handleStashDrop}, + {ViewName: "commitMessage", Key: gocui.KeyEnter, Modifier: gocui.ModNone, Handler: gui.handleCommitConfirm}, + {ViewName: "commitMessage", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: gui.handleCommitClose}, + {ViewName: "commitMessage", Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: gui.handleNewlineCommitMessage}, } // Would make these keybindings global but that interferes with editing diff --git a/pkg/gui/panels/merge_panel.go b/pkg/gui/merge_panel.go similarity index 84% rename from pkg/gui/panels/merge_panel.go rename to pkg/gui/merge_panel.go index e15608829..7e0a3a873 100644 --- a/pkg/gui/panels/merge_panel.go +++ b/pkg/gui/merge_panel.go @@ -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 -package panels +package gui import ( "bufio" @@ -12,12 +12,14 @@ import ( "github.com/fatih/color" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/utils" ) -func findConflicts(content string) ([]conflict, error) { - conflicts := make([]conflict, 0) +func findConflicts(content string) ([]commands.Conflict, error) { + conflicts := make([]commands.Conflict, 0) 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" { newConflict = conflict{start: i} } else if line == "=======" { @@ -30,15 +32,15 @@ func findConflicts(content string) ([]conflict, error) { return conflicts, nil } -func shiftConflict(conflicts []conflict) (conflict, []conflict) { +func shiftConflict(conflicts []commands.Conflict) (commands.Conflict, []commands.Conflict) { 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) } -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 { return content, nil } @@ -87,7 +89,7 @@ func handleSelectPrevConflict(g *gocui.Gui, v *gocui.View) error { 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 || i == conflict.start || i == conflict.end || @@ -96,8 +98,8 @@ func isIndexToDelete(i int, conflict conflict, pick string) bool { (pick == "top" && i > conflict.middle && i < conflict.end) } -func resolveConflict(g *gocui.Gui, conflict conflict, pick string) error { - gitFile, err := getSelectedFile(g) +func resolveConflict(g *gocui.Gui, conflict commands.Conflict, pick string) error { + gitFile, err := gui.getSelectedFile(g) if err != nil { return err } @@ -123,7 +125,7 @@ func resolveConflict(g *gocui.Gui, conflict conflict, pick string) error { } func pushFileSnapshot(g *gocui.Gui) error { - gitFile, err := getSelectedFile(g) + gitFile, err := gui.getSelectedFile(g) if err != nil { return err } @@ -140,7 +142,7 @@ func handlePopFileSnapshot(g *gocui.Gui, v *gocui.View) error { return nil } prevContent := state.EditHistory.Pop().(string) - gitFile, err := getSelectedFile(g) + gitFile, err := gui.getSelectedFile(g) if err != nil { return err } @@ -204,7 +206,7 @@ func refreshMergePanel(g *gocui.Gui) error { if err := scrollToConflict(g); err != nil { return err } - return renderString(g, "main", content) + return gui.renderString(g, "main", content) } func scrollToConflict(g *gocui.Gui) error { @@ -234,7 +236,7 @@ func switchToMerging(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", "← →": "navigate conflicts", "space": "pick hunk", @@ -248,8 +250,8 @@ func handleEscapeMerge(g *gocui.Gui, v *gocui.View) error { if err != nil { return err } - refreshFiles(g) - return switchFocus(g, v, filesView) + gui.refreshFiles(g) + return gui.switchFocus(g, v, filesView) } func handleCompleteMerge(g *gocui.Gui) error { @@ -258,6 +260,6 @@ func handleCompleteMerge(g *gocui.Gui) error { return err } stageSelectedFile(g) - refreshFiles(g) - return switchFocus(g, nil, filesView) + gui.refreshFiles(g) + return gui.switchFocus(g, nil, filesView) } diff --git a/pkg/gui/panels/branches_panel.go b/pkg/gui/panels/branches_panel.go deleted file mode 100644 index 63e3ad7e8..000000000 --- a/pkg/gui/panels/branches_panel.go +++ /dev/null @@ -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 -} diff --git a/pkg/gui/panels/commit_message_panel.go b/pkg/gui/panels/commit_message_panel.go deleted file mode 100644 index 12634c574..000000000 --- a/pkg/gui/panels/commit_message_panel.go +++ /dev/null @@ -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") -} diff --git a/pkg/gui/panels/commits_panel.go b/pkg/gui/panels/commits_panel.go deleted file mode 100644 index 96f897d96..000000000 --- a/pkg/gui/panels/commits_panel.go +++ /dev/null @@ -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 -} diff --git a/pkg/gui/panels/files_panel.go b/pkg/gui/panels/files_panel.go deleted file mode 100644 index 12dfde91e..000000000 --- a/pkg/gui/panels/files_panel.go +++ /dev/null @@ -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) -} diff --git a/pkg/gui/panels/stash_panel.go b/pkg/gui/stash_panel.go similarity index 69% rename from pkg/gui/panels/stash_panel.go rename to pkg/gui/stash_panel.go index 045bf3794..1af6bcc29 100644 --- a/pkg/gui/panels/stash_panel.go +++ b/pkg/gui/stash_panel.go @@ -1,4 +1,4 @@ -package panels +package gui import ( "fmt" @@ -17,7 +17,7 @@ func refreshStashEntries(g *gocui.Gui) error { for _, stashEntry := range state.StashEntries { fmt.Fprintln(v, stashEntry.DisplayString) } - return resetOrigin(v) + return gui.resetOrigin(v) }) return nil } @@ -26,12 +26,12 @@ func getSelectedStashEntry(v *gocui.View) *StashEntry { if len(state.StashEntries) == 0 { return nil } - lineNumber := getItemPosition(v) + lineNumber := gui.getItemPosition(v) return &state.StashEntries[lineNumber] } func renderStashOptions(g *gocui.Gui) error { - return renderOptionsMap(g, map[string]string{ + return gui.renderOptionsMap(g, map[string]string{ "space": "apply", "g": "pop", "d": "drop", @@ -46,11 +46,11 @@ func handleStashEntrySelect(g *gocui.Gui, v *gocui.View) error { go func() { stashEntry := getSelectedStashEntry(v) if stashEntry == nil { - renderString(g, "main", "No stash entries") + gui.renderString(g, "main", "No stash entries") return } diff, _ := getStashEntryDiff(stashEntry.Index) - renderString(g, "main", diff) + gui.renderString(g, "main", diff) }() return nil } @@ -64,7 +64,7 @@ func handleStashPop(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") }, 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 { stashEntry := getSelectedStashEntry(v) 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 { - createErrorPanel(g, output) + gui.createErrorPanel(g, output) } refreshStashEntries(g) - return refreshFiles(g) + return gui.refreshFiles(g) } func handleStashSave(g *gocui.Gui, filesView *gocui.View) error { - createPromptPanel(g, filesView, "Stash changes", func(g *gocui.Gui, v *gocui.View) error { - if output, err := gitStashSave(trimmedContent(v)); err != nil { - createErrorPanel(g, output) + gui.createPromptPanel(g, filesView, "Stash changes", func(g *gocui.Gui, v *gocui.View) error { + if output, err := gitStashSave(gui.trimmedContent(v)); err != nil { + gui.createErrorPanel(g, output) } refreshStashEntries(g) - return refreshFiles(g) + return gui.refreshFiles(g) }) return nil } diff --git a/pkg/gui/panels/status_panel.go b/pkg/gui/status_panel.go similarity index 98% rename from pkg/gui/panels/status_panel.go rename to pkg/gui/status_panel.go index 673bad802..822be2846 100644 --- a/pkg/gui/panels/status_panel.go +++ b/pkg/gui/status_panel.go @@ -1,4 +1,4 @@ -package panels +package gui import ( "fmt" diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index fc252a25c..a9564de5e 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -11,14 +11,14 @@ import ( var cyclableViews = []string{"files", "branches", "commits", "stash"} -func refreshSidePanels(g *gocui.Gui) error { - refreshBranches(g) - refreshFiles(g) - refreshCommits(g) +func (gui *Gui) refreshSidePanels(g *gocui.Gui) error { + gui.refreshBranches(g) + gui.gui.refreshFiles(g) + gui.refreshCommits(g) 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 if v == nil || v.Name() == cyclableViews[len(cyclableViews)-1] { focusedViewName = cyclableViews[0] @@ -29,7 +29,7 @@ func nextView(g *gocui.Gui, v *gocui.View) error { break } 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 } } @@ -38,10 +38,10 @@ func nextView(g *gocui.Gui, v *gocui.View) error { if err != nil { 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 if v == nil || v.Name() == cyclableViews[0] { focusedViewName = cyclableViews[len(cyclableViews)-1] @@ -61,16 +61,16 @@ func previousView(g *gocui.Gui, v *gocui.View) error { if err != nil { 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.SetOrigin(0, 0) switch v.Name() { case "files": - return handleFileSelect(g, v) + return gui.handleFileSelect(g, v) case "branches": return handleBranchSelect(g, v) 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) if err != nil { 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 -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 should never stack confirmation panels if oldView != nil && oldView.Name() != "confirmation" { @@ -117,13 +117,13 @@ func switchFocus(g *gocui.Gui, oldView, newView *gocui.View) error { return newLineFocused(g, newView) } -func getItemPosition(v *gocui.View) int { +func (gui *Gui) getItemPosition(v *gocui.View) int { _, cy := v.Cursor() _, oy := v.Origin() 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 // TODO: pull this out if v == nil || v.Name() == "main" { @@ -142,7 +142,7 @@ func cursorUp(g *gocui.Gui, v *gocui.View) error { 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 // TODO: pull this out if v == nil || v.Name() == "main" { @@ -163,15 +163,15 @@ func cursorDown(g *gocui.Gui, v *gocui.View) error { return nil } -func resetOrigin(v *gocui.View) error { +func (gui *Gui) resetOrigin(v *gocui.View) error { if err := v.SetCursor(0, 0); err != nil { return err } return v.SetOrigin(0, 0) } -// if the cursor down past the last item, move it up one -func correctCursor(v *gocui.View) error { +// if the cursor down past the last item, move it to the last line +func (gui *Gui) correctCursor(v *gocui.View) error { cx, cy := v.Cursor() _, oy := v.Origin() lineCount := len(v.BufferLines()) - 2 @@ -181,7 +181,7 @@ func correctCursor(v *gocui.View) error { 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 { v, err := g.View(viewName) // 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 } -func optionsMapToString(optionsMap map[string]string) string { +func (gui *Gui) optionsMapToString(optionsMap map[string]string) string { optionsArray := make([]string, 0) for key, description := range optionsMap { optionsArray = append(optionsArray, key+": "+description) @@ -206,11 +206,11 @@ func optionsMapToString(optionsMap map[string]string) string { return strings.Join(optionsArray, ", ") } -func renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) error { - return renderString(g, "options", optionsMapToString(optionsMap)) +func (gui *Gui) renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) error { + return gui.renderString(g, "options", optionsMapToString(optionsMap)) } -func loader() string { +func (gui *Gui) loader() string { characters := "|/-\\" now := time.Now() nanos := now.UnixNano() @@ -219,21 +219,21 @@ func loader() string { } // TODO: refactor properly -func getFilesView(g *gocui.Gui) *gocui.View { +func (gui *Gui) getFilesView(g *gocui.Gui) *gocui.View { v, _ := g.View("files") return v } -func getCommitsView(g *gocui.Gui) *gocui.View { +func (gui *Gui) getCommitsView(g *gocui.Gui) *gocui.View { v, _ := g.View("commits") return v } -func getCommitMessageView(g *gocui.Gui) *gocui.View { +func (gui *Gui) getCommitMessageView(g *gocui.Gui) *gocui.View { v, _ := g.View("commitMessage") return v } -func trimmedContent(v *gocui.View) string { +func (gui *Gui) trimmedContent(v *gocui.View) string { return strings.TrimSpace(v.Buffer()) } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 6b91ed6ef..b3efcc2db 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -54,3 +54,12 @@ func GetCurrentProject() string { } 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 +} diff --git a/utils.go b/utils.go deleted file mode 100644 index df7d04818..000000000 --- a/utils.go +++ /dev/null @@ -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) -}