feat: rebase support

This commit is contained in:
Jan De Dobbeleer 2020-10-07 13:32:42 +02:00 committed by Jan De Dobbeleer
parent e0adc584e5
commit 9f23514268
6 changed files with 175 additions and 25 deletions

View file

@ -28,7 +28,8 @@ Local changes can also shown by default using the following syntax for both the
"branch_ahead_icon": "↑",
"branch_behind_icon": "↓",
"local_working_icon": "",
"local_staged_icon": ""
"local_staged_icon": "",
"rebase_icon": " "
}
}
```
@ -42,3 +43,4 @@ Local changes can also shown by default using the following syntax for both the
- local_working_icon: `string` - the icon to display in front of the working area changes
- local_staged_icon: `string` - the icon to display in front of the staged area changes
- display_status: `boolean` - display the local changes or not
- rebase_icon: `string` - icon/text to display before the context when in a rebase

View file

@ -1,6 +1,7 @@
package main
import (
"io/ioutil"
"log"
"os"
"os/exec"
@ -18,6 +19,8 @@ type environmentInfo interface {
getcwd() string
homeDir() string
hasFiles(pattern string) bool
hasFolder(folder string) bool
getFileContent(file string) string
getPathSeperator() string
getCurrentUser() (*user.User, error)
isRunningAsRoot() bool
@ -69,6 +72,19 @@ func (env *environment) hasFiles(pattern string) bool {
return len(matches) > 0
}
func (env *environment) hasFolder(folder string) bool {
_, err := os.Stat(folder)
return !os.IsNotExist(err)
}
func (env *environment) getFileContent(file string) string {
content, err := ioutil.ReadFile(file)
if err != nil {
return ""
}
return string(content)
}
func (env *environment) getPathSeperator() string {
return string(os.PathSeparator)
}

View file

@ -62,8 +62,6 @@ func (g *git) enabled() bool {
func (g *git) string() string {
g.getGitStatus()
buffer := new(bytes.Buffer)
// branchsymbol
buffer.WriteString(g.props.getString(BranchIcon, "Branch: "))
// branchName
fmt.Fprintf(buffer, "%s", g.repo.branch)
displayStatus := g.props.getBool(DisplayStatus, true)
@ -101,7 +99,7 @@ func (g *git) init(props *properties, env environmentInfo) {
func (g *git) getGitStatus() {
g.repo = &gitRepo{}
output := g.getGitOutputForCommand("status", "--porcelain", "-b", "--ignore-submodules")
output := g.getGitCommandOutput("status", "--porcelain", "-b", "--ignore-submodules")
splittedOutput := strings.Split(output, "\n")
g.repo.working = g.parseGitStats(splittedOutput, true)
g.repo.staging = g.parseGitStats(splittedOutput, false)
@ -109,35 +107,69 @@ func (g *git) getGitStatus() {
if branchInfo["local"] != "" {
g.repo.ahead, _ = strconv.Atoi(branchInfo["ahead"])
g.repo.behind, _ = strconv.Atoi(branchInfo["behind"])
g.repo.branch = branchInfo["local"]
g.repo.branch = fmt.Sprintf("%s%s", g.props.getString(BranchIcon, "Branch:"), branchInfo["local"])
g.repo.upstream = branchInfo["upstream"]
} else {
g.repo.branch = g.getGitDetachedBranch()
g.repo.branch = g.getGitDetachedBranchContext()
}
g.repo.stashCount = g.getStashContext()
}
func (g *git) getGitOutputForCommand(args ...string) string {
func (g *git) getGitCommandOutput(args ...string) string {
args = append([]string{"-c", "core.quotepath=false", "-c", "color.status=false"}, args...)
return g.env.runCommand("git", args...)
}
func (g *git) getGitDetachedBranch() string {
commit := g.getGitOutputForCommand("rev-parse", "--short", "HEAD")
rebase := g.getGitOutputForCommand("rebase", "--show-current-patch")
func (g *git) getGitDetachedBranchContext() string {
commit := g.getGitCommandOutput("rev-parse", "--short", "HEAD")
rebase := g.getGitCommandOutput("rebase", "--show-current-patch")
if rebase != "" {
return fmt.Sprintf("%s%s", g.props.getString(RebaseIcon, "REBASE: "), commit)
return g.getGitRebaseContext(commit)
}
ref := g.getGitOutputForCommand("symbolic-ref", "-q", "--short", "HEAD")
// name of branch
ref := g.getGitCommandOutput("symbolic-ref", "-q", "--short", "HEAD")
if ref == "" {
ref = g.getGitOutputForCommand("describe", "--tags", "--exact-match")
// get a tag name if there's a match for HEAD
ref = g.getGitCommandOutput("describe", "--tags", "--exact-match")
}
if ref == "" {
// revert to the short commit hash
ref = commit
}
return ref
}
func (g *git) getGitRebaseContext(commit string) string {
if g.env.hasFolder(".git/rebase-merge") {
origin := g.getGitRefFileSymbolicName("rebase-merge/orig-head")
onto := g.getGitRefFileSymbolicName("rebase-merge/onto")
step := g.getGitFileContents("rebase-merge/msgnum")
total := g.getGitFileContents("rebase-merge/end")
icon := g.props.getString(RebaseIcon, "REBASE:")
return fmt.Sprintf("%s%s onto %s (%s/%s) at %s", icon, origin, onto, step, total, commit)
}
if g.env.hasFolder(".git/rebase-apply") {
head := g.getGitFileContents("rebase-apply/head-name")
origin := strings.Replace(head, "refs/heads/", "", 1)
step := g.getGitFileContents("rebase-apply/next")
total := g.getGitFileContents("rebase-apply/last")
icon := g.props.getString(RebaseIcon, "REBASING:")
return fmt.Sprintf("%s%s (%s/%s) at %s", icon, origin, step, total, commit)
}
icon := g.props.getString(RebaseIcon, "REBASE:")
return fmt.Sprintf("%sUNKNOWN", icon)
}
func (g *git) getGitFileContents(file string) string {
content := g.env.getFileContent(fmt.Sprintf(".git/%s", file))
return strings.Trim(content, " \r\n")
}
func (g *git) getGitRefFileSymbolicName(refFile string) string {
ref := g.getGitFileContents(refFile)
return g.getGitCommandOutput("name-rev", "--name-only", "--exclude=tags/*", ref)
}
func (g *git) parseGitStats(output []string, working bool) *gitStatus {
status := gitStatus{}
if len(output) <= 1 {
@ -168,7 +200,7 @@ func (g *git) parseGitStats(output []string, working bool) *gitStatus {
}
func (g *git) getStashContext() int {
stash := g.getGitOutputForCommand("stash", "list")
stash := g.getGitCommandOutput("stash", "list")
return numberOfLinesInString(stash)
}

View file

@ -35,29 +35,118 @@ func TestGetGitOutputForCommand(t *testing.T) {
g := &git{
env: env,
}
got := g.getGitOutputForCommand(commandArgs...)
got := g.getGitCommandOutput(commandArgs...)
assert.Equal(t, want, got)
}
func TestGetGitDetachedBranch(t *testing.T) {
want := "master"
type detachedContext struct {
currentCommit string
rebase string
rebaseMerge bool
rebaseApply bool
origin string
onto string
step string
total string
branchName string
tagName string
}
func setupDetachedHeadEnv(context *detachedContext) environmentInfo {
env := new(MockedEnvironment)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "symbolic-ref", "--short", "HEAD"}).Return(want)
env.On("hasFolder", ".git/rebase-merge").Return(context.rebaseMerge)
env.On("hasFolder", ".git/rebase-apply").Return(context.rebaseApply)
env.On("getFileContent", ".git/rebase-merge/orig-head").Return(context.origin)
env.On("getFileContent", ".git/rebase-merge/onto").Return(context.onto)
env.On("getFileContent", ".git/rebase-merge/msgnum").Return(context.step)
env.On("getFileContent", ".git/rebase-apply/next").Return(context.step)
env.On("getFileContent", ".git/rebase-merge/end").Return(context.total)
env.On("getFileContent", ".git/rebase-apply/last").Return(context.total)
env.On("getFileContent", ".git/rebase-apply/head-name").Return(context.origin)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "rev-parse", "--short", "HEAD"}).Return(context.currentCommit)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "rebase", "--show-current-patch"}).Return(context.rebase)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "symbolic-ref", "-q", "--short", "HEAD"}).Return(context.branchName)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "describe", "--tags", "--exact-match"}).Return(context.tagName)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "name-rev", "--name-only", "--exclude=tags/*", context.origin}).Return(context.origin)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "name-rev", "--name-only", "--exclude=tags/*", context.onto}).Return(context.onto)
return env
}
func TestGetGitDetachedCommitHash(t *testing.T) {
want := "lalasha1"
context := &detachedContext{
currentCommit: want,
}
env := setupDetachedHeadEnv(context)
g := &git{
env: env,
}
got := g.getGitDetachedBranch()
got := g.getGitDetachedBranchContext()
assert.Equal(t, want, got)
}
func TestGetGitDetachedBranchEmpty(t *testing.T) {
want := "unknown"
env := new(MockedEnvironment)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "symbolic-ref", "--short", "HEAD"}).Return("")
func TestGetGitDetachedTagName(t *testing.T) {
want := "lalasha1"
context := &detachedContext{
currentCommit: "whatever",
tagName: want,
}
env := setupDetachedHeadEnv(context)
g := &git{
env: env,
}
got := g.getGitDetachedBranch()
got := g.getGitDetachedBranchContext()
assert.Equal(t, want, got)
}
func TestGetGitDetachedRebaseMerge(t *testing.T) {
want := "REBASE:cool-feature-bro onto main (2/3) at whatever"
context := &detachedContext{
currentCommit: "whatever",
rebase: "true",
rebaseMerge: true,
origin: "cool-feature-bro",
onto: "main",
step: "2",
total: "3",
}
env := setupDetachedHeadEnv(context)
g := &git{
env: env,
}
got := g.getGitDetachedBranchContext()
assert.Equal(t, want, got)
}
func TestGetGitDetachedRebaseApply(t *testing.T) {
want := "REBASING:cool-feature-bro (2/3) at whatever"
context := &detachedContext{
currentCommit: "whatever",
rebase: "true",
rebaseApply: true,
origin: "cool-feature-bro",
step: "2",
total: "3",
}
env := setupDetachedHeadEnv(context)
g := &git{
env: env,
}
got := g.getGitDetachedBranchContext()
assert.Equal(t, want, got)
}
func TestGetGitDetachedRebaseUnknown(t *testing.T) {
want := "REBASE:UNKNOWN"
context := &detachedContext{
currentCommit: "whatever",
rebase: "true",
}
env := setupDetachedHeadEnv(context)
g := &git{
env: env,
}
got := g.getGitDetachedBranchContext()
assert.Equal(t, want, got)
}

View file

@ -34,6 +34,16 @@ func (env *MockedEnvironment) hasFiles(pattern string) bool {
return args.Bool(0)
}
func (env *MockedEnvironment) hasFolder(folder string) bool {
args := env.Called(folder)
return args.Bool(0)
}
func (env *MockedEnvironment) getFileContent(file string) string {
args := env.Called(file)
return args.String(0)
}
func (env *MockedEnvironment) getPathSeperator() string {
args := env.Called(nil)
return args.String(0)

View file

@ -52,7 +52,8 @@
"branch_behind_icon": "↓",
"branch_gone_icon": "≢",
"local_working_icon": "",
"local_staged_icon": ""
"local_staged_icon": "",
"rebase_icon": " "
}
},
{