summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2018-08-13 20:26:02 +1000
committerJesse Duffield <jessedduffield@gmail.com>2018-08-13 20:26:02 +1000
commit97cff656121270e9c790432e28622d92ab7b0f1a (patch)
tree7425013e94dc0a19699b48bde6bb20c6f5b86c8a
parentf9c39ad64bddd1577636c0ce5606eda44bc704ef (diff)
progress on refactor
-rw-r--r--pkg/app/app.go7
-rw-r--r--pkg/commands/branch.go (renamed from pkg/git/branch.go)16
-rw-r--r--pkg/commands/git.go92
-rw-r--r--pkg/commands/git_structs.go12
-rw-r--r--pkg/commands/os.go10
-rw-r--r--pkg/git/branch_list_builder.go63
-rw-r--r--pkg/gui/branches_panel.go141
-rw-r--r--pkg/gui/commit_message_panel.go50
-rw-r--r--pkg/gui/commits_panel.go176
-rw-r--r--pkg/gui/confirmation_panel.go (renamed from pkg/gui/panels/confirmation_panel.go)74
-rw-r--r--pkg/gui/files_panel.go383
-rw-r--r--pkg/gui/gui.go138
-rw-r--r--pkg/gui/keybindings.go102
-rw-r--r--pkg/gui/merge_panel.go (renamed from pkg/gui/panels/merge_panel.go)38
-rw-r--r--pkg/gui/panels/branches_panel.go136
-rw-r--r--pkg/gui/panels/commit_message_panel.go45
-rw-r--r--pkg/gui/panels/commits_panel.go176
-rw-r--r--pkg/gui/panels/files_panel.go373
-rw-r--r--pkg/gui/stash_panel.go (renamed from pkg/gui/panels/stash_panel.go)28
-rw-r--r--pkg/gui/status_panel.go (renamed from pkg/gui/panels/status_panel.go)2
-rw-r--r--pkg/gui/view_helpers.go58
-rw-r--r--pkg/utils/utils.go9
-rw-r--r--utils.go49
23 files changed, 1098 insertions, 1080 deletions
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
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.Squ