2019-03-13 04:14:30 -07:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
|
|
|
"regexp"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
type gitRepo struct {
|
|
|
|
working *gitStatus
|
|
|
|
staging *gitStatus
|
|
|
|
ahead int
|
|
|
|
behind int
|
|
|
|
branch string
|
|
|
|
upstream string
|
|
|
|
stashCount int
|
|
|
|
}
|
|
|
|
|
|
|
|
type gitStatus struct {
|
|
|
|
unmerged int
|
|
|
|
deleted int
|
|
|
|
added int
|
|
|
|
modified int
|
|
|
|
untracked int
|
|
|
|
}
|
|
|
|
|
|
|
|
type git struct {
|
|
|
|
props *properties
|
|
|
|
env environmentInfo
|
|
|
|
repo *gitRepo
|
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
//BranchIcon the icon to use as branch indicator
|
|
|
|
BranchIcon Property = "branch_icon"
|
|
|
|
//BranchIdenticalIcon the icon to display when the remote and local branch are identical
|
|
|
|
BranchIdenticalIcon Property = "branch_identical_icon"
|
|
|
|
//BranchAheadIcon the icon to display when the local branch is ahead of the remote
|
|
|
|
BranchAheadIcon Property = "branch_ahead_icon"
|
|
|
|
//BranchBehindIcon the icon to display when the local branch is behind the remote
|
|
|
|
BranchBehindIcon Property = "branch_behind_icon"
|
|
|
|
//LocalWorkingIcon the icon to use as the local working area changes indicator
|
|
|
|
LocalWorkingIcon Property = "local_working_icon"
|
|
|
|
//LocalStagingIcon the icon to use as the local staging area changes indicator
|
|
|
|
LocalStagingIcon Property = "local_staged_icon"
|
|
|
|
//DisplayStatus shows the status of the repository
|
|
|
|
DisplayStatus Property = "display_status"
|
2020-10-07 02:13:42 -07:00
|
|
|
//RebaseIcon shows before the rebase context
|
|
|
|
RebaseIcon Property = "rebase_icon"
|
2020-10-07 11:44:22 -07:00
|
|
|
//CherryPickIcon shows before the cherry-pick context
|
|
|
|
CherryPickIcon Property = "cherry_pick_icon"
|
|
|
|
//DetachedIcon shows before the detached context
|
|
|
|
DetachedIcon Property = "detached_icon"
|
|
|
|
//TagIcon shows before the tag context
|
|
|
|
TagIcon Property = "tag_icon"
|
2019-03-13 04:14:30 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
func (g *git) enabled() bool {
|
|
|
|
if !g.env.hasCommand("git") {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
output := g.env.runCommand("git", "rev-parse", "--is-inside-work-tree")
|
|
|
|
return output == "true"
|
|
|
|
}
|
|
|
|
|
|
|
|
func (g *git) string() string {
|
|
|
|
g.getGitStatus()
|
|
|
|
buffer := new(bytes.Buffer)
|
|
|
|
// branchName
|
|
|
|
fmt.Fprintf(buffer, "%s", g.repo.branch)
|
|
|
|
displayStatus := g.props.getBool(DisplayStatus, true)
|
|
|
|
if !displayStatus {
|
|
|
|
return buffer.String()
|
|
|
|
}
|
|
|
|
// TODO: Add upstream gone icon
|
|
|
|
// if ahead, print with symbol
|
|
|
|
if g.repo.ahead > 0 {
|
|
|
|
fmt.Fprintf(buffer, " %s%d", g.props.getString(BranchAheadIcon, "+"), g.repo.ahead)
|
|
|
|
}
|
|
|
|
// if behind, print with symbol
|
|
|
|
if g.repo.behind > 0 {
|
|
|
|
fmt.Fprintf(buffer, " %s%d", g.props.getString(BranchBehindIcon, "-"), g.repo.behind)
|
|
|
|
}
|
|
|
|
if g.repo.behind == 0 && g.repo.ahead == 0 {
|
|
|
|
fmt.Fprintf(buffer, " %s", g.props.getString(BranchIdenticalIcon, "="))
|
|
|
|
}
|
|
|
|
// if staging, print that part
|
|
|
|
if g.hasStaging() {
|
|
|
|
fmt.Fprintf(buffer, " %s +%d ~%d -%d", g.props.getString(LocalStagingIcon, "~"), g.repo.staging.added, g.repo.staging.modified, g.repo.staging.deleted)
|
|
|
|
}
|
|
|
|
// if working, print that part
|
|
|
|
if g.hasWorking() {
|
|
|
|
fmt.Fprintf(buffer, " %s +%d ~%d -%d", g.props.getString(LocalWorkingIcon, "#"), g.repo.working.added+g.repo.working.untracked, g.repo.working.modified, g.repo.working.deleted)
|
|
|
|
}
|
|
|
|
// TODO: Add stash entries
|
|
|
|
return buffer.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (g *git) init(props *properties, env environmentInfo) {
|
|
|
|
g.props = props
|
|
|
|
g.env = env
|
|
|
|
}
|
|
|
|
|
|
|
|
func (g *git) getGitStatus() {
|
|
|
|
g.repo = &gitRepo{}
|
2020-10-07 04:32:42 -07:00
|
|
|
output := g.getGitCommandOutput("status", "--porcelain", "-b", "--ignore-submodules")
|
2019-03-13 04:14:30 -07:00
|
|
|
splittedOutput := strings.Split(output, "\n")
|
|
|
|
g.repo.working = g.parseGitStats(splittedOutput, true)
|
|
|
|
g.repo.staging = g.parseGitStats(splittedOutput, false)
|
2020-10-07 11:44:22 -07:00
|
|
|
status := g.parseGitStatusInfo(splittedOutput[0])
|
|
|
|
if status["local"] != "" {
|
|
|
|
g.repo.ahead, _ = strconv.Atoi(status["ahead"])
|
|
|
|
g.repo.behind, _ = strconv.Atoi(status["behind"])
|
|
|
|
g.repo.upstream = status["upstream"]
|
2019-03-13 04:14:30 -07:00
|
|
|
}
|
2020-10-07 11:44:22 -07:00
|
|
|
g.repo.branch = g.getGitHEADContext()
|
2019-03-13 04:14:30 -07:00
|
|
|
g.repo.stashCount = g.getStashContext()
|
|
|
|
}
|
|
|
|
|
2020-10-07 04:32:42 -07:00
|
|
|
func (g *git) getGitCommandOutput(args ...string) string {
|
2019-03-13 04:14:30 -07:00
|
|
|
args = append([]string{"-c", "core.quotepath=false", "-c", "color.status=false"}, args...)
|
|
|
|
return g.env.runCommand("git", args...)
|
|
|
|
}
|
|
|
|
|
2020-10-07 11:44:22 -07:00
|
|
|
func (g *git) getGitHEADContext() string {
|
2020-10-07 04:32:42 -07:00
|
|
|
commit := g.getGitCommandOutput("rev-parse", "--short", "HEAD")
|
|
|
|
rebase := g.getGitCommandOutput("rebase", "--show-current-patch")
|
2020-10-07 02:13:42 -07:00
|
|
|
if rebase != "" {
|
2020-10-07 04:32:42 -07:00
|
|
|
return g.getGitRebaseContext(commit)
|
2020-10-07 02:13:42 -07:00
|
|
|
}
|
2020-10-07 11:44:22 -07:00
|
|
|
// branch
|
|
|
|
icon := g.props.getString(BranchIcon, "BRANCH:")
|
2020-10-07 04:32:42 -07:00
|
|
|
ref := g.getGitCommandOutput("symbolic-ref", "-q", "--short", "HEAD")
|
2020-10-07 02:13:42 -07:00
|
|
|
if ref == "" {
|
2020-10-07 04:32:42 -07:00
|
|
|
// get a tag name if there's a match for HEAD
|
2020-10-07 11:44:22 -07:00
|
|
|
icon = g.props.getString(TagIcon, "TAG:")
|
2020-10-07 04:32:42 -07:00
|
|
|
ref = g.getGitCommandOutput("describe", "--tags", "--exact-match")
|
2020-10-07 02:13:42 -07:00
|
|
|
}
|
2020-10-07 11:44:22 -07:00
|
|
|
// validate additional context
|
|
|
|
if g.env.hasFiles(".git/CHERRY_PICK_HEAD") {
|
|
|
|
sha := g.getGitRefFileSymbolicName("CHERRY_PICK_HEAD")
|
|
|
|
icon := g.props.getString(CherryPickIcon, "CHERRY PICK:")
|
|
|
|
return fmt.Sprintf("%s%s onto %s", icon, sha, ref)
|
|
|
|
}
|
2019-03-13 04:14:30 -07:00
|
|
|
if ref == "" {
|
2020-10-07 02:13:42 -07:00
|
|
|
ref = commit
|
2020-10-07 11:44:22 -07:00
|
|
|
icon = g.props.getString(DetachedIcon, "DETACHED:")
|
2019-03-13 04:14:30 -07:00
|
|
|
}
|
2020-10-07 11:44:22 -07:00
|
|
|
return fmt.Sprintf("%s%s", icon, ref)
|
2019-03-13 04:14:30 -07:00
|
|
|
}
|
|
|
|
|
2020-10-07 04:32:42 -07:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2019-03-13 04:14:30 -07:00
|
|
|
func (g *git) parseGitStats(output []string, working bool) *gitStatus {
|
|
|
|
status := gitStatus{}
|
|
|
|
if len(output) <= 1 {
|
|
|
|
return &status
|
|
|
|
}
|
|
|
|
for _, line := range output[1:] {
|
|
|
|
if len(line) < 2 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
code := line[0:1]
|
|
|
|
if working {
|
|
|
|
code = line[1:2]
|
|
|
|
}
|
|
|
|
switch code {
|
|
|
|
case "?":
|
|
|
|
status.untracked++
|
|
|
|
case "D":
|
|
|
|
status.deleted++
|
|
|
|
case "A":
|
|
|
|
status.added++
|
|
|
|
case "U":
|
|
|
|
status.unmerged++
|
|
|
|
case "M", "R", "C":
|
|
|
|
status.modified++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return &status
|
|
|
|
}
|
|
|
|
|
|
|
|
func (g *git) getStashContext() int {
|
2020-10-07 04:32:42 -07:00
|
|
|
stash := g.getGitCommandOutput("stash", "list")
|
2019-03-13 04:14:30 -07:00
|
|
|
return numberOfLinesInString(stash)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (g *git) hasStaging() bool {
|
|
|
|
return g.repo.staging.deleted > 0 || g.repo.staging.added > 0 || g.repo.staging.unmerged > 0 || g.repo.staging.modified > 0
|
|
|
|
}
|
|
|
|
|
|
|
|
func (g *git) hasWorking() bool {
|
|
|
|
return g.repo.working.deleted > 0 || g.repo.working.added > 0 || g.repo.working.unmerged > 0 || g.repo.working.modified > 0 || g.repo.working.untracked > 0
|
|
|
|
}
|
|
|
|
|
2020-10-07 11:44:22 -07:00
|
|
|
func (g *git) parseGitStatusInfo(branchInfo string) map[string]string {
|
2019-03-13 04:14:30 -07:00
|
|
|
var branchRegex = regexp.MustCompile(`^## (?P<local>\S+?)(\.{3}(?P<upstream>\S+?)( \[(ahead (?P<ahead>\d+)(, )?)?(behind (?P<behind>\d+))?])?)?$`)
|
|
|
|
return groupDict(branchRegex, branchInfo)
|
|
|
|
}
|
|
|
|
|
|
|
|
func groupDict(pattern *regexp.Regexp, haystack string) map[string]string {
|
|
|
|
match := pattern.FindStringSubmatch(haystack)
|
|
|
|
result := make(map[string]string)
|
|
|
|
if len(match) > 0 {
|
|
|
|
for i, name := range pattern.SubexpNames() {
|
|
|
|
if i != 0 {
|
|
|
|
result[name] = match[i]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
func numberOfLinesInString(s string) int {
|
|
|
|
n := 0
|
|
|
|
for _, r := range s {
|
|
|
|
if r == '\n' {
|
|
|
|
n++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(s) > 0 && !strings.HasSuffix(s, "\n") {
|
|
|
|
n++
|
|
|
|
}
|
|
|
|
return n
|
|
|
|
}
|