1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-02-03 13:21:56 +02:00

merge branch master

This commit is contained in:
Jesse Duffield 2018-08-11 15:09:37 +10:00
parent e46f3b3393
commit 7e141283f6
17 changed files with 388 additions and 251 deletions

1
Gopkg.lock generated
View File

@ -288,6 +288,7 @@
"github.com/jesseduffield/gocui",
"github.com/tcnksm/go-gitconfig",
"gopkg.in/src-d/go-git.v4",
"gopkg.in/src-d/go-git.v4/plumbing",
"gopkg.in/src-d/go-git.v4/plumbing/object",
]
solver-name = "gps-cdcl"

View File

@ -8,6 +8,8 @@ Are YOU tired of typing every git command directly into the terminal, but you're
![Gif](https://image.ibb.co/mmeXho/optimisedgif.gif)
[Twitch Stream](https://www.twitch.tv/jesseduffield)
## Installation
### Homebrew
@ -74,3 +76,6 @@ We love your input! Please check out the [contributing guide](CONTRIBUTING.md).
## Work in progress
This is still a work in progress so there's still bugs to iron out and as this is my first project in Go the code could no doubt use an increase in quality, but I'll be improving on it whenever I find the time. If you have any feedback feel free to [raise an issue](https://github.com/jesseduffield/lazygit/issues)/[submit a PR](https://github.com/jesseduffield/lazygit/pulls).
## Social
If you want to see what I (Jesse) am up to in terms of development, follow me on [twitter](https://twitter.com/DuffieldJesse) or watch me program on [twitch](https://www.twitch.tv/jesseduffield)

35
branch.go Normal file
View File

@ -0,0 +1,35 @@
package main
import (
"strings"
"github.com/fatih/color"
)
// Branch : A git branch
type Branch struct {
Name string
Recency string
}
func (b *Branch) getDisplayString() string {
return withPadding(b.Recency, 4) + coloredString(b.Name, b.getColor())
}
func (b *Branch) getColor() color.Attribute {
switch b.getType() {
case "feature":
return color.FgGreen
case "bugfix":
return color.FgYellow
case "hotfix":
return color.FgRed
default:
return color.FgWhite
}
}
// expected to return feature/bugfix/hotfix or blank string
func (b *Branch) getType() string {
return strings.Split(b.Name, "/")[0]
}

123
branch_list_builder.go Normal file
View File

@ -0,0 +1,123 @@
package main
import (
"regexp"
"strings"
"gopkg.in/src-d/go-git.v4/plumbing"
)
// context:
// we want to only show 'safe' branches (ones that haven't e.g. been deleted)
// which `git branch -a` gives us, but we also want the recency data that
// git reflog gives us.
// So we get the HEAD, then append get the reflog branches that intersect with
// our safe branches, then add the remaining safe branches, ensuring uniqueness
// along the way
type branchListBuilder struct{}
func newBranchListBuilder() *branchListBuilder {
return &branchListBuilder{}
}
func (b *branchListBuilder) obtainCurrentBranch() 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: " *"}
}
func (*branchListBuilder) obtainReflogBranches() []Branch {
branches := make([]Branch, 0)
rawString, err := runDirectCommand("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
if err != nil {
return branches
}
branchLines := splitLines(rawString)
for _, line := range branchLines {
timeNumber, timeUnit, branchName := branchInfoFromLine(line)
timeUnit = abbreviatedTimeUnit(timeUnit)
branch := Branch{Name: branchName, Recency: timeNumber + timeUnit}
branches = append(branches, branch)
}
return branches
}
func (b *branchListBuilder) obtainSafeBranches() []Branch {
branches := make([]Branch, 0)
bIter, err := r.Branches()
if err != nil {
panic(err)
}
err = bIter.ForEach(func(b *plumbing.Reference) error {
name := b.Name().Short()
branches = append(branches, Branch{Name: name})
return nil
})
return branches
}
func (b *branchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []Branch, included bool) []Branch {
for _, newBranch := range newBranches {
if included == branchIncluded(newBranch.Name, existingBranches) {
finalBranches = append(finalBranches, newBranch)
}
}
return finalBranches
}
func (b *branchListBuilder) build() []Branch {
branches := make([]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...))
branches = b.appendNewBranches(branches, reflogBranches, safeBranches, true)
branches = b.appendNewBranches(branches, safeBranches, branches, false)
return branches
}
func uniqueByName(branches []Branch) []Branch {
finalBranches := make([]Branch, 0)
for _, branch := range branches {
if branchIncluded(branch.Name, finalBranches) {
continue
}
finalBranches = append(finalBranches, branch)
}
return finalBranches
}
// A line will have the form '10 days ago master' so we need to strip out the
// useful information from that into timeNumber, timeUnit, and branchName
func branchInfoFromLine(line string) (string, string, string) {
r := regexp.MustCompile("\\|.*\\s")
line = r.ReplaceAllString(line, " ")
words := strings.Split(line, " ")
return words[0], words[1], words[3]
}
func abbreviatedTimeUnit(timeUnit string) string {
r := regexp.MustCompile("s$")
timeUnit = r.ReplaceAllString(timeUnit, "")
timeUnitMap := map[string]string{
"hour": "h",
"minute": "m",
"second": "s",
"week": "w",
"year": "y",
"day": "d",
"month": "m",
}
return timeUnitMap[timeUnit]
}

View File

@ -91,7 +91,7 @@ func handleBranchSelect(g *gocui.Gui, v *gocui.View) error {
}
go func() {
branch := getSelectedBranch(v)
diff, err := getBranchGraph(branch.Name, branch.BaseBranch)
diff, err := getBranchGraph(branch.Name)
if err != nil && strings.HasPrefix(diff, "fatal: ambiguous argument") {
diff = "There is no tracking for this branch"
}
@ -111,7 +111,7 @@ func refreshBranches(g *gocui.Gui) error {
state.Branches = getGitBranches()
v.Clear()
for _, branch := range state.Branches {
fmt.Fprintln(v, branch.DisplayString)
fmt.Fprintln(v, branch.getDisplayString())
}
resetOrigin(v)
return refreshStatus(g)

43
commit_message_panel.go Normal file
View File

@ -0,0 +1,43 @@
package main
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)
g.SetViewOnBottom("commitMessage")
switchFocus(g, v, getFilesView(g))
return refreshCommits(g)
}
func handleCommitClose(g *gocui.Gui, v *gocui.View) error {
g.SetViewOnBottom("commitMessage")
return switchFocus(g, v, getFilesView(g))
}
func handleNewlineCommitMessage(g *gocui.Gui, v *gocui.View) error {
// resising ahead of time so that the top line doesn't get hidden to make
// room for the cursor on the second line
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, v.Buffer())
if _, err := g.SetView("commitMessage", x0, y0, x1, y1+1, 0); err != nil {
if err != gocui.ErrUnknownView {
return err
}
}
v.EditNewLine()
return nil
}
func handleCommitFocused(g *gocui.Gui, v *gocui.View) error {
return renderString(g, "options", "esc: close, enter: confirm")
}

View File

@ -56,6 +56,7 @@ func getConfirmationPanelDimensions(g *gocui.Gui, prompt string) (int, int, int,
}
func 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, "")
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil {
@ -63,11 +64,8 @@ func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, hand
return err
}
g.Cursor = true
confirmationView.Editable = true
confirmationView.Title = title
confirmationView.FgColor = gocui.ColorWhite
switchFocus(g, currentView, confirmationView)
return setKeyBindings(g, handleConfirm, nil)
}
@ -75,6 +73,7 @@ func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, hand
}
func 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 {
@ -141,15 +140,20 @@ func trimTrailingNewline(str string) string {
return str
}
func resizeConfirmationPanel(g *gocui.Gui) error {
func resizeConfirmationPanel(g *gocui.Gui, viewName string) error {
// If the confirmation panel is already displayed, just resize the width,
// otherwise continue
if v, err := g.View("confirmation"); err == nil {
g.Update(func(g *gocui.Gui) error {
v, err := g.View(viewName)
if err != nil {
return nil
}
content := trimTrailingNewline(v.Buffer())
x0, y0, x1, y1 := getConfirmationPanelDimensions(g, content)
if _, err = g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil {
if _, err := g.SetView(viewName, x0, y0, x1, y1, 0); err != nil {
return err
}
}
return nil
})
return nil
}

View File

@ -177,19 +177,11 @@ 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")
}
createPromptPanel(g, filesView, "Commit message", func(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)
return refreshCommits(g)
commitMessageView := getCommitMessageView(g)
g.Update(func(g *gocui.Gui) error {
g.SetViewOnTop("commitMessage")
switchFocus(g, filesView, commitMessageView)
return nil
})
return nil
}

View File

@ -7,11 +7,9 @@ import (
"fmt"
"os"
"os/exec"
"regexp"
"strings"
"time"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
gitconfig "github.com/tcnksm/go-gitconfig"
git "gopkg.in/src-d/go-git.v4"
@ -19,9 +17,6 @@ import (
)
var (
// ErrNoCheckedOutBranch : When we have no checked out branch
ErrNoCheckedOutBranch = errors.New("No currently checked out branch")
// 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")
)
@ -38,14 +33,6 @@ type GitFile struct {
DisplayString string
}
// Branch : A git branch
type Branch struct {
Name string
Type string
BaseBranch string
DisplayString string
}
// Commit : A git commit
type Commit struct {
Sha string
@ -144,29 +131,6 @@ func branchStringParts(branchString string) (string, string) {
return splitBranchName[0], splitBranchName[1]
}
// branchPropertiesFromName : returns branch type, base, and color
func branchPropertiesFromName(name string) (string, string, color.Attribute) {
if strings.Contains(name, "feature/") {
return "feature", "develop", color.FgGreen
} else if strings.Contains(name, "bugfix/") {
return "bugfix", "develop", color.FgYellow
} else if strings.Contains(name, "hotfix/") {
return "hotfix", "master", color.FgRed
}
return "other", name, color.FgWhite
}
func coloredString(str string, colour *color.Color) string {
return colour.SprintFunc()(fmt.Sprint(str))
}
func withPadding(str string, padding int) string {
if padding-len(str) < 0 {
return str
}
return str + strings.Repeat(" ", padding-len(str))
}
// TODO: DRY up this function and getGitBranches
func getGitStashEntries() []StashEntry {
stashEntries := make([]StashEntry, 0)
@ -325,12 +289,8 @@ func runSubProcess(g *gocui.Gui, cmdName string, commandArgs ...string) {
})
}
func getBranchGraph(branch string, baseBranch string) (string, error) {
func getBranchGraph(branch string) (string, error) {
return runCommand("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 " + branch)
// Leaving this guy commented out in case there's backlash from the design
// change and I want to make this configurable
// return runCommand("git log -p -30 --color --no-merges " + branch)
}
func verifyInGitRepo() {
@ -476,11 +436,7 @@ func gitPull() (string, error) {
}
func gitPush() (string, error) {
branchName := gitCurrentBranchName()
if branchName == "" {
return "", ErrNoCheckedOutBranch
}
return runDirectCommand("git push -u origin " + branchName)
return runDirectCommand("git push -u origin " + state.Branches[0].Name)
}
func gitSquashPreviousTwoCommits(message string) (string, error) {
@ -562,120 +518,20 @@ func gitCommitsToPush() []string {
return splitLines(pushables)
}
func gitCurrentBranchName() string {
branchName, err := runDirectCommand("git symbolic-ref --short HEAD")
// if there is an error, assume there are no branches yet
if err != nil {
return ""
}
return strings.TrimSpace(branchName)
}
// A line will have the form '10 days ago master' so we need to strip out the
// useful information from that into timeNumber, timeUnit, and branchName
func branchInfoFromLine(line string) (string, string, string) {
r := regexp.MustCompile("\\|.*\\s")
line = r.ReplaceAllString(line, " ")
words := strings.Split(line, " ")
return words[0], words[1], words[3]
}
func abbreviatedTimeUnit(timeUnit string) string {
r := regexp.MustCompile("s$")
timeUnit = r.ReplaceAllString(timeUnit, "")
timeUnitMap := map[string]string{
"hour": "h",
"minute": "m",
"second": "s",
"week": "w",
"year": "y",
"day": "d",
"month": "m",
}
return timeUnitMap[timeUnit]
}
func getBranches() []Branch {
branches := make([]Branch, 0)
rawString, err := runDirectCommand("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD")
if err != nil {
return branches
}
branchLines := splitLines(rawString)
for i, line := range branchLines {
timeNumber, timeUnit, branchName := branchInfoFromLine(line)
timeUnit = abbreviatedTimeUnit(timeUnit)
if branchAlreadyStored(branchName, branches) {
continue
}
branch := constructBranch(timeNumber+timeUnit, branchName, i)
branches = append(branches, branch)
}
return branches
}
func constructBranch(prefix, name string, index int) Branch {
branchType, branchBase, colourAttr := branchPropertiesFromName(name)
if index == 0 {
prefix = " *"
}
colour := color.New(colourAttr)
displayString := withPadding(prefix, 4) + coloredString(name, colour)
return Branch{
Name: name,
Type: branchType,
BaseBranch: branchBase,
DisplayString: displayString,
}
}
func getGitBranches() []Branch {
// check if there are any branches
branchCheck, _ := runCommand("git branch")
if branchCheck == "" {
return []Branch{constructBranch("", gitCurrentBranchName(), 0)}
}
branches := getBranches()
if len(branches) == 0 {
branches = append(branches, constructBranch("", gitCurrentBranchName(), 0))
}
branches = getAndMergeFetchedBranches(branches)
return branches
builder := newBranchListBuilder()
return builder.build()
}
func branchAlreadyStored(branchName string, branches []Branch) bool {
func branchIncluded(branchName string, branches []Branch) bool {
for _, existingBranch := range branches {
if existingBranch.Name == branchName {
if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) {
return true
}
}
return false
}
// here branches contains all the branches that we've checked out, along with
// the recency. In this function we append the branches that are in our heads
// directory i.e. things we've fetched but haven't necessarily checked out.
// Worth mentioning this has nothing to do with the 'git merge' operation
func getAndMergeFetchedBranches(branches []Branch) []Branch {
rawString, err := runDirectCommand("git branch --sort=-committerdate --no-color")
if err != nil {
return branches
}
branchLines := splitLines(rawString)
for _, line := range branchLines {
line = strings.Replace(line, "* ", "", -1)
line = strings.TrimSpace(line)
if branchAlreadyStored(line, branches) {
continue
}
branches = append(branches, constructBranch("", line, len(branches)))
}
return branches
}
func gitResetHard() error {
return w.Reset(&git.ResetOptions{Mode: git.HardReset})
}

21
gui.go
View File

@ -199,12 +199,27 @@ func layout(g *gocui.Gui) error {
if err != gocui.ErrUnknownView {
return err
}
v.BgColor = gocui.ColorDefault
v.FgColor = gocui.ColorBlue
v.Frame = false
}
if err = resizeConfirmationPanel(g); err != nil {
if 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 {
return err
}
g.SetViewOnBottom("commitMessage")
commitMessageView.Title = "Commit message"
commitMessageView.FgColor = gocui.ColorWhite
commitMessageView.Editable = true
}
}
if err = resizeConfirmationPanel(g, "commitMessage"); err != nil {
return err
}
if err = resizeConfirmationPanel(g, "confirmation"); err != nil {
return err
}
@ -261,6 +276,8 @@ func run() (err error) {
}
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)

View File

@ -14,66 +14,69 @@ type Binding struct {
func keybindings(g *gocui.Gui) error {
bindings := []Binding{
Binding{ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: quit},
Binding{ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: quit},
Binding{ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: scrollUpMain},
Binding{ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: scrollDownMain},
Binding{ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: pushFiles},
Binding{ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: pullFiles},
Binding{ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: handleRefresh},
Binding{ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: handleCommitPress},
Binding{ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleFilePress},
Binding{ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: handleFileRemove},
Binding{ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: handleSwitchToMerge},
Binding{ViewName: "files", Key: 'e', Modifier: gocui.ModNone, Handler: handleFileEdit},
Binding{ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: handleFileOpen},
Binding{ViewName: "files", Key: 's', Modifier: gocui.ModNone, Handler: handleSublimeFileOpen},
Binding{ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: handleVsCodeFileOpen},
Binding{ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: handleIgnoreFile},
Binding{ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: handleRefreshFiles},
Binding{ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: handleStashSave},
Binding{ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: handleAbortMerge},
Binding{ViewName: "files", Key: 't', Modifier: gocui.ModNone, Handler: handleAddPatch},
Binding{ViewName: "files", Key: 'D', Modifier: gocui.ModNone, Handler: handleResetHard},
Binding{ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleEscapeMerge},
Binding{ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handlePickHunk},
Binding{ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: handlePickBothHunks},
Binding{ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: handleSelectPrevConflict},
Binding{ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: handleSelectNextConflict},
Binding{ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: handleSelectTop},
Binding{ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: handleSelectBottom},
Binding{ViewName: "main", Key: 'h', Modifier: gocui.ModNone, Handler: handleSelectPrevConflict},
Binding{ViewName: "main", Key: 'l', Modifier: gocui.ModNone, Handler: handleSelectNextConflict},
Binding{ViewName: "main", Key: 'k', Modifier: gocui.ModNone, Handler: handleSelectTop},
Binding{ViewName: "main", Key: 'j', Modifier: gocui.ModNone, Handler: handleSelectBottom},
Binding{ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: handlePopFileSnapshot},
Binding{ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleBranchPress},
Binding{ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: handleCheckoutByName},
Binding{ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: handleForceCheckout},
Binding{ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: handleNewBranch},
Binding{ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: handleMerge},
Binding{ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: handleCommitSquashDown},
Binding{ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: handleRenameCommit},
Binding{ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: handleResetToCommit},
Binding{ViewName: "commits", Key: 'f', Modifier: gocui.ModNone, Handler: handleCommitFixup},
Binding{ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleStashApply},
Binding{ViewName: "stash", Key: 'g', Modifier: gocui.ModNone, Handler: handleStashPop},
Binding{ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: handleStashDrop},
{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: 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: '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},
}
// Would make these keybindings global but that interferes with editing
// input in the confirmation panel
for _, viewName := range []string{"files", "branches", "commits", "stash"} {
bindings = append(bindings, []Binding{
Binding{ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: nextView},
Binding{ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: previousView},
Binding{ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: nextView},
Binding{ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: cursorUp},
Binding{ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: cursorDown},
Binding{ViewName: viewName, Key: 'h', Modifier: gocui.ModNone, Handler: previousView},
Binding{ViewName: viewName, Key: 'l', Modifier: gocui.ModNone, Handler: nextView},
Binding{ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: cursorUp},
Binding{ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: cursorDown},
{ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: nextView},
{ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: previousView},
{ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: nextView},
{ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: cursorUp},
{ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: cursorDown},
{ViewName: viewName, Key: 'h', Modifier: gocui.ModNone, Handler: previousView},
{ViewName: viewName, Key: 'l', Modifier: gocui.ModNone, Handler: nextView},
{ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: cursorUp},
{ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: cursorDown},
}...)
}

View File

@ -30,6 +30,7 @@ var (
versionFlag = flag.Bool("v", false, "Print the current version")
w *git.Worktree
r *git.Repository
)
func homeDirectory() string {
@ -98,7 +99,8 @@ func fallbackVersion() string {
}
func setupWorktree() {
r, err := git.PlainOpen(".")
var err error
r, err = git.PlainOpen(".")
if err != nil {
panic(err)
}

View File

@ -56,7 +56,7 @@ func coloredConflictFile(content string, conflicts []conflict, conflictIndex int
if i == conflict.end && len(remainingConflicts) > 0 {
conflict, remainingConflicts = shiftConflict(remainingConflicts)
}
outputBuffer.WriteString(coloredString(line, colour) + "\n")
outputBuffer.WriteString(coloredStringDirect(line, colour) + "\n")
}
return outputBuffer.String(), nil
}

View File

@ -24,16 +24,16 @@ func refreshStatus(g *gocui.Gui) error {
return err
}
if state.HasMergeConflicts {
colour := color.New(color.FgYellow)
fmt.Fprint(v, coloredString(" (merging)", colour))
fmt.Fprint(v, coloredString(" (merging)", color.FgYellow))
}
if len(branches) == 0 {
return nil
}
branch := branches[0]
// utilising the fact these all have padding to only grab the name
// from the display string with the existing coloring applied
fmt.Fprint(v, " "+branch.DisplayString[4:])
name := coloredString(branch.Name, branch.getColor())
repo := getCurrentProject()
fmt.Fprint(v, " "+repo+" → "+name)
return nil
})

54
utils.go Normal file
View File

@ -0,0 +1,54 @@
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/fatih/color"
"github.com/jesseduffield/gocui"
)
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 trimmedContent(v *gocui.View) string {
return strings.TrimSpace(v.Buffer())
}
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)
}

View File

@ -75,6 +75,8 @@ func newLineFocused(g *gocui.Gui, v *gocui.View) error {
return handleBranchSelect(g, v)
case "confirmation":
return nil
case "commitMessage":
return handleCommitFocused(g, v)
case "main":
// TODO: pull this out into a 'view focused' function
refreshMergePanel(g)
@ -121,10 +123,6 @@ func getItemPosition(v *gocui.View) int {
return oy + cy
}
func trimmedContent(v *gocui.View) string {
return strings.TrimSpace(v.Buffer())
}
func cursorUp(g *gocui.Gui, v *gocui.View) error {
// swallowing cursor movements in main
// TODO: pull this out
@ -199,18 +197,6 @@ func renderString(g *gocui.Gui, viewName, s string) error {
return nil
}
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 optionsMapToString(optionsMap map[string]string) string {
optionsArray := make([]string, 0)
for key, description := range optionsMap {
@ -231,3 +217,19 @@ func loader() string {
index := nanos / 50000000 % int64(len(characters))
return characters[index : index+1]
}
// TODO: refactor properly
func getFilesView(g *gocui.Gui) *gocui.View {
v, _ := g.View("files")
return v
}
func getCommitsView(g *gocui.Gui) *gocui.View {
v, _ := g.View("commits")
return v
}
func getCommitMessageView(g *gocui.Gui) *gocui.View {
v, _ := g.View("commitMessage")
return v
}