oh-my-posh/segment_git.go

248 lines
7.8 KiB
Go
Raw Normal View History

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
HEAD string
2019-03-13 04:14:30 -07:00
upstream string
2020-10-10 10:27:04 -07:00
stashCount string
2019-03-13 04:14:30 -07:00
}
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"
2020-10-10 10:16:58 -07:00
//BranchGoneIcon the icon to use when ther's no remote
BranchGoneIcon Property = "branch_gone_icon"
2019-03-13 04:14:30 -07:00
//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"
//CommitIcon shows before the detached context
CommitIcon Property = "commit_icon"
2020-10-07 11:44:22 -07:00
//TagIcon shows before the tag context
TagIcon Property = "tag_icon"
2020-10-10 10:27:04 -07:00
//DisplayStashCount show stash count or not
DisplayStashCount Property = "display_stash_count"
//StashCountIcon shows before the stash context
StashCountIcon Property = "stash_count_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.HEAD)
2019-03-13 04:14:30 -07:00
displayStatus := g.props.getBool(DisplayStatus, true)
if !displayStatus {
return buffer.String()
}
// 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)
}
2020-10-10 10:16:58 -07:00
if g.repo.behind == 0 && g.repo.ahead == 0 && g.repo.upstream != "" {
2019-03-13 04:14:30 -07:00
fmt.Fprintf(buffer, " %s", g.props.getString(BranchIdenticalIcon, "="))
2020-10-10 10:16:58 -07:00
} else if g.repo.upstream == "" {
fmt.Fprintf(buffer, " %s", g.props.getString(BranchGoneIcon, "!="))
2019-03-13 04:14:30 -07:00
}
// 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)
}
2020-10-10 10:27:04 -07:00
if g.props.getBool(DisplayStashCount, false) && g.repo.stashCount != "" {
fmt.Fprintf(buffer, " %s%s", g.props.getString(StashCountIcon, ""), g.repo.stashCount)
}
2019-03-13 04:14:30 -07:00
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
}
g.repo.HEAD = g.getGitHEADContext(status["local"])
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...)
}
func (g *git) getGitHEADContext(ref string) string {
branchIcon := g.props.getString(BranchIcon, "BRANCH:")
2020-10-07 02:13:42 -07:00
if ref == "" {
ref = g.getPrettyHEADName()
} else {
ref = fmt.Sprintf("%s%s", branchIcon, ref)
2020-10-07 02:13:42 -07:00
}
// rebase
2020-10-07 04:32:42 -07:00
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%s onto %s%s (%s/%s) at %s", icon, branchIcon, origin, branchIcon, onto, step, total, ref)
2020-10-07 04:32:42 -07:00
}
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/%s) at %s", icon, branchIcon, origin, step, total, ref)
}
// cherry-pick
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)
}
return ref
}
func (g *git) getPrettyHEADName() string {
// check for tag
ref := g.getGitCommandOutput("describe", "--tags", "--exact-match")
if ref != "" {
return fmt.Sprintf("%s%s", g.props.getString(TagIcon, "TAG:"), ref)
2020-10-07 04:32:42 -07:00
}
// fallback to commit
ref = g.getGitCommandOutput("rev-parse", "--short", "HEAD")
return fmt.Sprintf("%s%s", g.props.getString(CommitIcon, "COMMIT:"), ref)
2020-10-07 04:32:42 -07:00
}
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
}
2020-10-10 10:27:04 -07:00
func (g *git) getStashContext() string {
return g.getGitCommandOutput("rev-list", "--walk-reflogs", "--count", "refs/stash")
2019-03-13 04:14:30 -07:00
}
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
}