better handling of branches and untracked files

This commit is contained in:
Jesse Duffield
2018-07-21 15:51:18 +10:00
parent 211715fa77
commit dea751280e
10 changed files with 1158 additions and 1058 deletions

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@ commands.log
extra/lgit.rb
notes/go.notes
TODO.notes
TODO.md

View File

@@ -7,6 +7,10 @@ import (
)
func handleBranchPress(g *gocui.Gui, v *gocui.View) error {
index := getItemPosition(v)
if index == 0 {
return createErrorPanel(g, "You have already checked out this branch")
}
branch := getSelectedBranch(v)
if output, err := gitCheckout(branch.Name, false); err != nil {
createErrorPanel(g, output)
@@ -70,6 +74,7 @@ func renderBranchesOptions(g *gocui.Gui) error {
"f": "force checkout",
"m": "merge",
"c": "checkout by name",
"n": "new branch",
})
}

View File

@@ -38,7 +38,7 @@ func handleFilePress(g *gocui.Gui, v *gocui.View) error {
if file.HasUnstagedChanges {
stageFile(file.Name)
} else {
unStageFile(file.Name)
unStageFile(file.Name, file.Tracked)
}
if err := refreshFiles(g); err != nil {
@@ -101,6 +101,7 @@ func renderfilesOptions(g *gocui.Gui, gitFile *GitFile) error {
"c": "commit changes",
"o": "open",
"s": "open in sublime",
"v": "open in vscode",
"i": "ignore",
"d": "delete",
"space": "toggle staged",
@@ -125,7 +126,6 @@ func handleFileSelect(g *gocui.Gui, v *gocui.View) error {
return err
}
renderString(g, "main", "No changed files")
colorLog(color.FgRed, "error")
return renderfilesOptions(g, nil)
}
renderfilesOptions(g, &gitFile)
@@ -171,6 +171,9 @@ func handleFileOpen(g *gocui.Gui, v *gocui.View) error {
func handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error {
return genericFileOpen(g, v, sublimeOpenFile)
}
func handleVsCodeFileOpen(g *gocui.Gui, v *gocui.View) error {
return genericFileOpen(g, v, vsCodeOpenFile)
}
func refreshStateGitFiles() {
// get files to stage
@@ -193,7 +196,7 @@ func renderGitFile(gitFile GitFile, filesView *gocui.View) {
// objects with each render
red := color.New(color.FgRed)
green := color.New(color.FgGreen)
if !gitFile.Tracked {
if !gitFile.Tracked && !gitFile.HasStagedChanges {
red.Fprintln(filesView, gitFile.DisplayString)
return
}
@@ -296,5 +299,6 @@ func handleAbortMerge(g *gocui.Gui, v *gocui.View) error {
return createErrorPanel(g, output)
}
createMessagePanel(g, v, "", "Merge aborted")
refreshStatus(g)
return refreshFiles(g)
}

View File

@@ -110,14 +110,21 @@ func runDirectCommand(command string) (string, error) {
timeStart := time.Now()
commandLog(command)
cmdOut, err := exec.Command("bash", "-c", command).CombinedOutput()
cmdOut, err := exec.
Command("bash", "-c", command).
CombinedOutput()
devLog("run direct command time for command: ", command, time.Now().Sub(timeStart))
return string(cmdOut), err
}
func branchStringParts(branchString string) (string, string) {
// expect string to be something like '4w master`
splitBranchName := strings.Split(branchString, "\t")
// if we have no \t then we have no recency, so just output that as blank
if len(splitBranchName) == 1 {
return "", branchString
}
return splitBranchName[0], splitBranchName[1]
}
@@ -138,6 +145,9 @@ func coloredString(str string, colour *color.Color) string {
}
func withPadding(str string, padding int) string {
if padding-len(str) < 0 {
return str
}
return str + strings.Repeat(" ", padding-len(str))
}
@@ -162,19 +172,39 @@ func getGitBranches() []Branch {
// check if there are any branches
branchCheck, _ := runDirectCommand("git branch")
if branchCheck == "" {
return branches
return append(branches, branchFromLine("master", 0))
}
rawString, _ := runDirectCommand(getBranchesCommand)
branchLines := splitLines(rawString)
if len(branchLines) == 0 {
// sometimes the getBranchesCommand command returns nothing, in which case
// we assume you've just init'd or cloned the repo and you've got master
// checked out
branches = append(branches, branchFromLine(" *\tmaster", 0))
}
for i, line := range branchLines {
branches = append(branches, branchFromLine(line, i))
}
branches = getAndMergeFetchedBranches(branches)
return branches
}
func branchAlreadyStored(branchLine string, branches []Branch) bool {
for _, branch := range branches {
if branch.Name == branchLine {
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, _ := runDirectCommand(getHeadsCommand)
branchLines := splitLines(rawString)
for _, line := range branchLines {
if branchAlreadyStored(line, branches) {
continue
}
branches = append(branches, branchFromLine(line, len(branches)))
}
return branches
}
@@ -200,27 +230,42 @@ func getStashEntryDiff(index int) (string, error) {
return runCommand("git stash show -p --color stash@{" + fmt.Sprint(index) + "}")
}
func includes(array []string, str string) bool {
for _, arrayStr := range array {
if arrayStr == str {
return true
}
}
return false
}
func getGitStatusFiles() []GitFile {
statusOutput, _ := getGitStatus()
statusStrings := splitLines(statusOutput)
gitFiles := make([]GitFile, 0)
for _, statusString := range statusStrings {
stagedChange := statusString[0:1]
change := statusString[0:2]
stagedChange := change[0:1]
unstagedChange := statusString[1:2]
filename := statusString[3:]
tracked := statusString[0:2] != "??"
tracked := !includes([]string{"??", "A "}, change)
gitFile := GitFile{
Name: filename,
DisplayString: statusString,
HasStagedChanges: tracked && stagedChange != " " && stagedChange != "U",
HasUnstagedChanges: !tracked || unstagedChange != " ",
HasStagedChanges: !includes([]string{" ", "U", "?"}, stagedChange),
HasUnstagedChanges: unstagedChange != " ",
Tracked: tracked,
Deleted: unstagedChange == "D" || stagedChange == "D",
HasMergeConflicts: statusString[0:2] == "UU",
HasMergeConflicts: change == "UU",
}
devLog("tracked", gitFile.Tracked)
devLog("hasUnstagedChanges", gitFile.HasUnstagedChanges)
devLog("HasStagedChanges", gitFile.HasStagedChanges)
devLog("DisplayString", gitFile.DisplayString)
gitFiles = append(gitFiles, gitFile)
}
devLog(gitFiles)
return gitFiles
}
@@ -262,6 +307,10 @@ func openFile(filename string) (string, error) {
return runCommand("open " + filename)
}
func vsCodeOpenFile(filename string) (string, error) {
return runCommand("code -r " + filename)
}
func sublimeOpenFile(filename string) (string, error) {
return runCommand("subl " + filename)
}
@@ -324,7 +373,7 @@ func gitShow(sha string) string {
func getDiff(file GitFile) string {
cachedArg := ""
if file.HasStagedChanges {
if file.HasStagedChanges && !file.HasUnstagedChanges {
cachedArg = "--cached "
}
deletedArg := ""
@@ -332,10 +381,10 @@ func getDiff(file GitFile) string {
deletedArg = "-- "
}
trackedArg := ""
if !file.Tracked {
if !file.Tracked && !file.HasStagedChanges {
trackedArg = "--no-index /dev/null "
}
command := "git diff --color " + cachedArg + deletedArg + trackedArg + file.Name
command := "git diff -b --color " + cachedArg + deletedArg + trackedArg + file.Name
// for now we assume an error means the file was deleted
s, _ := runCommand(command)
return s
@@ -350,8 +399,15 @@ func stageFile(file string) error {
return err
}
func unStageFile(file string) error {
_, err := runCommand("git reset HEAD " + file)
func unStageFile(file string, tracked bool) error {
var command string
if tracked {
command = "git reset HEAD "
} else {
command = "git rm --cached "
}
devLog(command)
_, err := runCommand(command + file)
return err
}
@@ -444,7 +500,7 @@ func gitCommitsToPush() []string {
}
func gitCurrentBranchName() string {
branchName, err := runDirectCommand("git rev-parse --abbrev-ref HEAD")
branchName, err := runDirectCommand("git symbolic-ref --short HEAD")
// if there is an error, assume there are no branches yet
if err != nil {
return ""
@@ -465,6 +521,26 @@ git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD | {
printf "%s\t%s\n" "$date" "$branch"
fi
fi
done | sed 's/ days /d /g' | sed 's/ weeks /w /g' | sed 's/ hours /h /g' | sed 's/ minutes /m /g' | sed 's/ seconds /m /g' | sed 's/ago//g' | tr -d ' '
done \
| sed 's/ days /d /g' \
| sed 's/ day /d /g' \
| sed 's/ weeks /w /g' \
| sed 's/ week /w /g' \
| sed 's/ hours /h /g' \
| sed 's/ hour /h /g' \
| sed 's/ minutes /m /g' \
| sed 's/ minute /m /g' \
| sed 's/ seconds /s /g' \
| sed 's/ second /s /g' \
| sed 's/ago//g' \
| tr -d ' '
}
`
const getHeadsCommand = `git show-ref \
| grep 'refs/heads/\|refs/remotes/origin/' \
| sed 's/.*refs\/heads\///g' \
| sed 's/.*refs\/remotes\/origin\///g' \
| grep -v '^HEAD$' \
| sort \
| uniq`

4
gui.go
View File

@@ -7,6 +7,7 @@ import (
"log"
"time"
// "strings"
"github.com/golang-collections/collections/stack"
"github.com/jesseduffield/gocui"
@@ -113,6 +114,9 @@ func keybindings(g *gocui.Gui) error {
if err := g.SetKeybinding("files", 's', gocui.ModNone, handleSublimeFileOpen); err != nil {
return err
}
if err := g.SetKeybinding("files", 'v', gocui.ModNone, handleVsCodeFileOpen); err != nil {
return err
}
if err := g.SetKeybinding("files", 'i', gocui.ModNone, handleIgnoreFile); err != nil {
return err
}

10
main.go
View File

@@ -48,6 +48,15 @@ func localLog(colour color.Attribute, path string, objects ...interface{}) {
}
}
func navigateToRepoRootDirectory() {
_, err := os.Stat(".git")
for os.IsNotExist(err) {
devLog("going up a directory to find the root")
os.Chdir("..")
_, err = os.Stat(".git")
}
}
func main() {
debuggingPointer := flag.Bool("debug", false, "a boolean")
flag.Parse()
@@ -55,5 +64,6 @@ func main() {
devLog("\n\n\n\n\n\n\n\n\n\n")
startTime = time.Now()
verifyInGitRepo()
navigateToRepoRootDirectory()
run()
}

1
newFile Normal file
View File

@@ -0,0 +1 @@
newFile

1
newFile2 Normal file
View File

@@ -0,0 +1 @@
newFile

View File

@@ -66,7 +66,6 @@ func handleStashDrop(g *gocui.Gui, v *gocui.View) error {
return createConfirmationPanel(g, v, "Stash drop", "Are you sure you want to drop this stash entry? (y/n)", func(g *gocui.Gui, v *gocui.View) error {
return stashDo(g, v, "drop")
}, nil)
return nil
}
func stashDo(g *gocui.Gui, v *gocui.View, method string) error {

View File

@@ -36,7 +36,6 @@ func nextView(g *gocui.Gui, v *gocui.View) error {
focusedView, err := g.View(focusedViewName)
if err != nil {
panic(err)
return err
}
return switchFocus(g, v, focusedView)
}