diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2018-08-13 20:26:02 +1000 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2018-08-13 20:26:02 +1000 |
commit | 97cff656121270e9c790432e28622d92ab7b0f1a (patch) | |
tree | 7425013e94dc0a19699b48bde6bb20c6f5b86c8a | |
parent | f9c39ad64bddd1577636c0ce5606eda44bc704ef (diff) |
progress on refactor
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 |