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:
parent
e46f3b3393
commit
7e141283f6
1
Gopkg.lock
generated
1
Gopkg.lock
generated
@ -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"
|
||||
|
@ -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
35
branch.go
Normal 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
123
branch_list_builder.go
Normal 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]
|
||||
}
|
@ -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
43
commit_message_panel.go
Normal 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")
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
156
gitcommands.go
156
gitcommands.go
@ -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
21
gui.go
@ -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)
|
||||
|
111
keybindings.go
111
keybindings.go
@ -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},
|
||||
}...)
|
||||
}
|
||||
|
||||
|
4
main.go
4
main.go
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
54
utils.go
Normal 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)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user