feat(posh-git): parse environment variable

BREAKING CHANGE: this removes the posh-git segment. To mitigate,
rename the posh-git segment to git.

In case you had a custom template, make sure to migrate to the git
segment's template. You can now also leverage the same logic and
properties as the git segment in both the text template and/or
color templates.
This commit is contained in:
Jan De Dobbeleer 2022-09-07 07:02:23 +02:00 committed by Jan De Dobbeleer
parent 17f75fa1ff
commit 3ded4c00fc
10 changed files with 366 additions and 149 deletions

View file

@ -306,7 +306,6 @@ func (segment *Segment) mapSegmentWithWriter(env environment.Environment) error
PERL: &segments.Perl{},
PHP: &segments.Php{},
PLASTIC: &segments.Plastic{},
POSHGIT: &segments.PoshGit{},
PROJECT: &segments.Project{},
PYTHON: &segments.Python{},
R: &segments.R{},

View file

@ -104,6 +104,7 @@ type Git struct {
Upstream string
UpstreamIcon string
UpstreamURL string
RawUpstreamURL string
UpstreamGone bool
StashCount int
WorktreeCount int
@ -112,14 +113,23 @@ type Git struct {
}
func (g *Git) Template() string {
return " {{ .HEAD }} {{ .BranchStatus }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}{{ if and (.Staging.Changed) (.Working.Changed) }} |{{ end }}{{ if .Staging.Changed }} \uF046 {{ .Staging.String }}{{ end }}{{ if gt .StashCount 0}} \uF692 {{ .StashCount }}{{ end }}{{ if gt .WorktreeCount 0}} \uf1bb {{ .WorktreeCount }}{{ end }} " //nolint: lll
return " {{ .HEAD }}{{if .BranchStatus }} {{ .BranchStatus }}{{ end }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}{{ if and (.Staging.Changed) (.Working.Changed) }} |{{ end }}{{ if .Staging.Changed }} \uF046 {{ .Staging.String }}{{ end }}{{ if gt .StashCount 0}} \uF692 {{ .StashCount }}{{ end }}{{ if gt .WorktreeCount 0}} \uf1bb {{ .WorktreeCount }}{{ end }} " //nolint: lll
}
func (g *Git) Enabled() bool {
if !g.shouldDisplay() {
return false
}
g.RepoName = environment.Base(g.env, g.convertToLinuxPath(g.realDir))
if g.props.GetBool(FetchWorktreeCount, false) {
g.WorktreeCount = g.getWorktreeContext()
}
if g.hasPoshGitStatus() {
return true
}
displayStatus := g.props.GetBool(FetchStatus, false)
if displayStatus {
g.setGitStatus()
@ -130,22 +140,27 @@ func (g *Git) Enabled() bool {
g.Working = &GitStatus{}
g.Staging = &GitStatus{}
}
if g.Upstream != "" && g.props.GetBool(FetchUpstreamIcon, false) {
if len(g.Upstream) != 0 && g.props.GetBool(FetchUpstreamIcon, false) {
g.UpstreamIcon = g.getUpstreamIcon()
}
if g.props.GetBool(FetchStashCount, false) {
g.StashCount = g.getStashContext()
}
if g.props.GetBool(FetchWorktreeCount, false) {
g.WorktreeCount = g.getWorktreeContext()
}
return true
}
func (g *Git) Kraken() string {
root := g.getGitCommandOutput("rev-list", "--max-parents=0", "HEAD")
remote := g.getGitCommandOutput("remote", "get-url", "origin")
return fmt.Sprintf("gitkraken://repolink/%s/commit/%s?url=%s", root, g.Hash, url2.QueryEscape(remote))
if len(g.RawUpstreamURL) == 0 {
if len(g.Upstream) == 0 {
g.Upstream = "origin"
}
g.RawUpstreamURL = g.getOriginURL()
}
if len(g.Hash) == 0 {
g.Hash = g.getGitCommandOutput("rev-parse", "HEAD")
}
return fmt.Sprintf("gitkraken://repolink/%s/commit/%s?url=%s", root, g.Hash, url2.QueryEscape(g.RawUpstreamURL))
}
func (g *Git) shouldDisplay() bool {
@ -166,12 +181,7 @@ func (g *Git) shouldDisplay() bool {
return false
}
dir := environment.ReplaceHomeDirPrefixWithTilde(g.env, gitdir.Path) // align with template PWD
if g.env.GOOS() == environment.WINDOWS {
g.Dir = strings.TrimSuffix(dir, `\.git`)
} else {
g.Dir = strings.TrimSuffix(dir, "/.git")
}
g.setDir(gitdir.Path)
if !gitdir.IsDir {
return g.hasWorktree(gitdir)
@ -184,6 +194,15 @@ func (g *Git) shouldDisplay() bool {
return true
}
func (g *Git) setDir(dir string) {
dir = environment.ReplaceHomeDirPrefixWithTilde(g.env, dir) // align with template PWD
if g.env.GOOS() == environment.WINDOWS {
g.Dir = strings.TrimSuffix(dir, `\.git`)
return
}
g.Dir = strings.TrimSuffix(dir, "/.git")
}
func (g *Git) hasWorktree(gitdir *environment.FileInfo) bool {
g.rootDir = gitdir.Path
dirPointer := strings.Trim(g.env.FileContent(gitdir.Path), " \r\n")
@ -264,8 +283,18 @@ func (g *Git) setBranchStatus() {
}
func (g *Git) getUpstreamIcon() string {
upstream := regex.ReplaceAllString("/.*", g.Upstream, "")
g.UpstreamURL = g.getOriginURL(upstream)
cleanSSHURL := func(url string) string {
if strings.HasPrefix(url, "http") {
return url
}
url = strings.TrimPrefix(url, "git://")
url = strings.TrimPrefix(url, "git@")
url = strings.TrimSuffix(url, ".git")
url = strings.ReplaceAll(url, ":", "/")
return fmt.Sprintf("https://%s", url)
}
g.RawUpstreamURL = g.getOriginURL()
g.UpstreamURL = cleanSSHURL(g.RawUpstreamURL)
if strings.Contains(g.UpstreamURL, "github") {
return g.props.GetString(GithubIcon, "\uF408 ")
}
@ -539,28 +568,17 @@ func (g *Git) getWorktreeContext() int {
return count
}
func (g *Git) getOriginURL(upstream string) string {
cleanSSHURL := func(url string) string {
if strings.HasPrefix(url, "http") {
return url
}
url = strings.TrimPrefix(url, "git://")
url = strings.TrimPrefix(url, "git@")
url = strings.TrimSuffix(url, ".git")
url = strings.ReplaceAll(url, ":", "/")
return fmt.Sprintf("https://%s", url)
}
var url string
func (g *Git) getOriginURL() string {
upstream := regex.ReplaceAllString("/.*", g.Upstream, "")
cfg, err := ini.Load(g.rootDir + "/config")
if err != nil {
url = g.getGitCommandOutput("remote", "get-url", upstream)
return cleanSSHURL(url)
return g.getGitCommandOutput("remote", "get-url", upstream)
}
url = cfg.Section("remote \"" + upstream + "\"").Key("url").String()
if url == "" {
url := cfg.Section("remote \"" + upstream + "\"").Key("url").String()
if len(url) == 0 {
url = g.getGitCommandOutput("remote", "get-url", upstream)
}
return cleanSSHURL(url)
return url
}
func (g *Git) getUntrackedFilesMode() string {

View file

@ -48,6 +48,7 @@ func TestEnabledInWorkingDirectory(t *testing.T) {
env.On("HasParentFilePath", ".git").Return(fileInfo, nil)
env.On("PathSeparator").Return("/")
env.On("Home").Return("/Users/posh")
env.On("Getenv", poshGitEnv).Return("")
g := &Git{
scm: scm{
env: env,

View file

@ -1,33 +1,88 @@
package segments
import (
"oh-my-posh/environment"
"oh-my-posh/properties"
"encoding/json"
"fmt"
"strings"
)
type PoshGit struct {
props properties.Properties
env environment.Environment
Status string
}
const (
poshGitEnv = "POSH_GIT_STATUS"
)
func (p *PoshGit) Template() string {
return " {{ .Status }} "
type poshGit struct {
StashCount int `json:"StashCount"`
AheadBy int `json:"AheadBy"`
Index *poshGitStatus `json:"Index"`
RepoName string `json:"RepoName"`
HasWorking bool `json:"HasWorking"`
Branch string `json:"Branch"`
HasIndex bool `json:"HasIndex"`
GitDir string `json:"GitDir"`
BehindBy int `json:"BehindBy"`
HasUntracked bool `json:"HasUntracked"`
Working *poshGitStatus `json:"Working"`
Upstream string `json:"Upstream"`
}
func (p *PoshGit) Enabled() bool {
status := p.env.Getenv(poshGitEnv)
p.Status = strings.TrimSpace(status)
return p.Status != ""
type poshGitStatus struct {
Added []string `json:"Added"`
Modified []string `json:"Modified"`
Deleted []string `json:"Deleted"`
Unmerged []string `json:"Unmerged"`
}
func (p *PoshGit) Init(props properties.Properties, env environment.Environment) {
p.props = props
p.env = env
func (s *GitStatus) parsePoshGitStatus(p *poshGitStatus) {
if p == nil {
return
}
s.Added = len(p.Added)
s.Deleted = len(p.Deleted)
s.Modified = len(p.Modified)
s.Unmerged = len(p.Unmerged)
}
func (g *Git) hasPoshGitStatus() bool {
envStatus := g.env.Getenv(poshGitEnv)
if len(envStatus) == 0 {
return false
}
var posh poshGit
err := json.Unmarshal([]byte(envStatus), &posh)
if err != nil {
return false
}
g.setDir(posh.GitDir)
g.Working = &GitStatus{}
g.Working.parsePoshGitStatus(posh.Working)
g.Staging = &GitStatus{}
g.Staging.parsePoshGitStatus(posh.Index)
g.HEAD = g.parsePoshGitHEAD(posh.Branch)
g.StashCount = posh.StashCount
g.Ahead = posh.AheadBy
g.Behind = posh.BehindBy
g.UpstreamGone = len(posh.Upstream) == 0
g.Upstream = posh.Upstream
g.setBranchStatus()
if len(g.Upstream) != 0 && g.props.GetBool(FetchUpstreamIcon, false) {
g.UpstreamIcon = g.getUpstreamIcon()
}
return true
}
func (g *Git) parsePoshGitHEAD(head string) string {
// commit
if strings.HasSuffix(head, "...)") {
head = strings.TrimLeft(head, "(")
head = strings.TrimRight(head, ".)")
return fmt.Sprintf("%s%s", g.props.GetString(CommitIcon, "\uF417"), head)
}
// tag
if strings.HasPrefix(head, "(") {
head = strings.TrimLeft(head, "(")
head = strings.TrimRight(head, ")")
return fmt.Sprintf("%s%s", g.props.GetString(TagIcon, "\uF412"), head)
}
// regular branch
return fmt.Sprintf("%s%s", g.props.GetString(BranchIcon, "\uE0A0"), g.formatHEAD(head))
}

View file

@ -1,6 +1,7 @@
package segments
import (
"oh-my-posh/environment"
"oh-my-posh/mock"
"oh-my-posh/properties"
"testing"
@ -11,25 +12,228 @@ import (
func TestPoshGitSegment(t *testing.T) {
cases := []struct {
Case string
PoshGitPrompt string
Expected string
Enabled bool
PoshGitJSON string
FetchUpstreamIcon bool
Template string
ExpectedString string
ExpectedEnabled bool
}{
{Case: "regular prompt", PoshGitPrompt: "my prompt", Expected: "my prompt", Enabled: true},
{Case: "prompt with spaces", PoshGitPrompt: " my prompt", Expected: "my prompt", Enabled: true},
{Case: "no prompt", PoshGitPrompt: "", Enabled: false},
{
Case: "no status",
PoshGitJSON: "",
ExpectedString: "my prompt",
ExpectedEnabled: false,
},
{
Case: "invalid data",
PoshGitJSON: "{",
ExpectedString: "my prompt",
ExpectedEnabled: false,
},
{
Case: "Changes in Working",
PoshGitJSON: `
{
"RepoName": "oh-my-posh",
"HasIndex": false,
"GitDir": "/Users/bill/Code/oh-my-posh/.git",
"Upstream": "origin/posh-git-json",
"UpstreamGone": false,
"HasUntracked": false,
"AheadBy": 0,
"StashCount": 0,
"HasWorking": true,
"BehindBy": 0,
"Index": {
"value": [],
"Added": [],
"Modified": [],
"Deleted": [],
"Unmerged": []
},
"Working": {
"value": [
"../src/segments/git_test.go",
"../src/segments/posh_git_test.go"
],
"Added": [],
"Modified": [
"../src/segments/git_test.go",
"../src/segments/posh_git_test.go"
],
"Deleted": [],
"Unmerged": []
},
"Branch": "posh-git-json"
}
`,
ExpectedString: "\ue0a0posh-git-json ≡ \uf044 ~2",
ExpectedEnabled: true,
},
{
Case: "Changes in Working and Staging, branch ahead an behind",
PoshGitJSON: `
{
"RepoName": "oh-my-posh",
"HasIndex": false,
"GitDir": "/Users/bill/Code/oh-my-posh/.git",
"Upstream": "origin/posh-git-json",
"UpstreamGone": false,
"HasUntracked": false,
"AheadBy": 1,
"StashCount": 2,
"HasWorking": true,
"BehindBy": 1,
"Index": {
"value": [
"../src/segments/git_test.go",
"../src/segments/posh_git_test.go"
],
"Added": [],
"Deleted": [
"../src/segments/git_test.go",
"../src/segments/posh_git_test.go"
],
"Modified": [],
"Unmerged": []
},
"Working": {
"value": [
"../src/segments/git_test.go",
"../src/segments/posh_git_test.go"
],
"Added": [],
"Modified": [
"../src/segments/git_test.go",
"../src/segments/posh_git_test.go"
],
"Deleted": [],
"Unmerged": []
},
"Branch": "posh-git-json"
}
`,
ExpectedString: "\ue0a0posh-git-json ↑1 ↓1 \uf044 ~2 | \uf046 -2 \uf692 2",
ExpectedEnabled: true,
},
{
Case: "Clean branch, no upstream and stash count",
PoshGitJSON: `
{
"RepoName": "oh-my-posh",
"GitDir": "/Users/bill/Code/oh-my-posh/.git",
"StashCount": 2,
"Index": {
"value": [],
"Added": [],
"Modified": [],
"Deleted": [],
"Unmerged": []
},
"Working": {
"value": [],
"Added": [],
"Modified": [],
"Deleted": [],
"Unmerged": []
},
"Branch": "posh-git-json"
}
`,
ExpectedString: "\ue0a0posh-git-json ≢ \uf692 2",
ExpectedEnabled: true,
},
{
Case: "No working data",
PoshGitJSON: `
{
"RepoName": "oh-my-posh",
"GitDir": "/Users/bill/Code/oh-my-posh/.git",
"StashCount": 2,
"Index": {
"value": [],
"Added": [],
"Modified": [],
"Deleted": [],
"Unmerged": []
},
"Branch": "posh-git-json"
}
`,
ExpectedString: "\ue0a0posh-git-json ≢ \uf692 2",
ExpectedEnabled: true,
},
{
Case: "Fetch upstream icon (GitHub)",
Template: "{{ .UpstreamIcon }}",
PoshGitJSON: `
{
"RepoName": "oh-my-posh",
"GitDir": "/Users/bill/Code/oh-my-posh/.git",
"Branch": "\ue0a0posh-git-json",
"Upstream": "origin/posh-git-json"
}
`,
ExpectedString: "\uf408",
FetchUpstreamIcon: true,
ExpectedEnabled: true,
},
}
for _, tc := range cases {
env := new(mock.MockedEnvironment)
env.On("Getenv", poshGitEnv).Return(tc.PoshGitPrompt)
p := &PoshGit{
env.On("Getenv", poshGitEnv).Return(tc.PoshGitJSON)
env.On("Home").Return("/Users/bill")
env.On("GOOS").Return(environment.LINUX)
env.On("RunCommand", "git", []string{"-C", "", "--no-optional-locks", "-c", "core.quotepath=false",
"-c", "color.status=false", "remote", "get-url", "origin"}).Return("github.com/cli", nil)
g := &Git{
scm: scm{
env: env,
props: &properties.Map{
FetchUpstreamIcon: tc.FetchUpstreamIcon,
},
},
}
if len(tc.Template) == 0 {
tc.Template = g.Template()
}
assert.Equal(t, tc.ExpectedEnabled, g.hasPoshGitStatus(), tc.Case)
if tc.ExpectedEnabled {
assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, g), tc.Case)
}
}
}
func TestParsePoshGitHEAD(t *testing.T) {
cases := []struct {
Case string
HEAD string
ExpectedString string
}{
{
Case: "branch",
HEAD: "main",
ExpectedString: "\ue0a0main",
},
{
Case: "tag",
HEAD: "(tag)",
ExpectedString: "\uf412tag",
},
{
Case: "commit",
HEAD: "(commit...)",
ExpectedString: "\uf417commit",
},
}
for _, tc := range cases {
g := &Git{
scm: scm{
props: &properties.Map{},
},
}
assert.Equal(t, tc.Enabled, p.Enabled(), tc.Case)
if tc.Enabled {
assert.Equal(t, tc.Expected, renderTemplate(env, p.Template(), p), tc.Case)
}
assert.Equal(t, tc.ExpectedString, g.parsePoshGitHEAD(tc.HEAD), tc.Case)
}
}

View file

@ -97,7 +97,7 @@ New-Module -Name "oh-my-posh-core" -ScriptBlock {
if ($env:POSH_GIT_ENABLED -eq $true) {
# We need to set the status so posh-git can facilitate autocomplete
$global:GitStatus = Get-GitStatus
$env:POSH_GIT_STATUS = Write-GitStatus -Status $global:GitStatus
$env:POSH_GIT_STATUS = $global:GitStatus | ConvertTo-Json
}
}
@ -338,6 +338,9 @@ Example:
# the output can be multiline, joining these ensures proper rendering by adding line breaks with `n
$standardOut -join "`n"
# remove any posh-git status
$env:POSH_GIT_STATUS = $null
# restore the orignal last exit code
$global:LASTEXITCODE = $script:OriginalLastExitCode
}

View file

@ -1915,19 +1915,6 @@
}
}
},
{
"if": {
"properties": {
"type": {
"const": "poshgit"
}
}
},
"then": {
"title": "Posh-Git Segment",
"description": "https://ohmyposh.dev/docs/segments/poshgit"
}
},
{
"if": {
"properties": {

View file

@ -17,15 +17,9 @@ Local changes can also be displayed which uses the following syntax for both the
- `?` untracked
:::tip
PowerShell offers support for the `posh-git` module for autocompletion, but it is disabled by default.
To enable this, set `$env:POSH_GIT_ENABLED = $true` in your `$PROFILE`. Be aware this calls `git status`
twice, so in case you're having a slow repository, it's better to migrate to the [Posh-Git segment][poshgit].
:::
:::caution
Starting from version 3.152.0, `fetch_status` is disabled by default.
It improves performance but reduces the quantity of information. Don't forget to enable it in your theme if needed.
An alternative is to use the [Posh-Git segment][poshgit].
PowerShell offers support for the [posh-git][poshgit] module for autocompletion, but it is disabled by default.
To enable this, set `$env:POSH_GIT_ENABLED = $true` in your `$PROFILE`. This will also make use of the
[posh-git][poshgit] output rather than do additional work to get the git status.
:::
## Sample Configuration

View file

@ -1,43 +0,0 @@
---
id: poshgit
title: Posh-Git
sidebar_label: Git (posh-git)
---
## What
Display the [posh-git][posh-git] prompt.
:::caution
This segment only works within PowerShell and requires the posh-git module to be installed and imported
as well as `$env:POSH_GIT_ENABLED = $true` added to your `$PROFILE`.
:::
## Sample Configuration
```json
{
"type": "poshgit",
"style": "powerline",
"powerline_symbol": "\uE0B0",
"foreground": "#ffffff",
"background": "#0077c2"
}
```
## Template ([info][templates])
:::note default template
``` template
{{ .Status }}
```
:::
### Properties
- `.Status`: `string` - the status reported from posh-git
[posh-git]: https://github.com/dahlbyk/posh-git
[templates]: /docs/configuration/templates

View file

@ -71,7 +71,6 @@ module.exports = {
"segments/fossil",
"segments/gcp",
"segments/git",
"segments/poshgit",
"segments/golang",
"segments/haskell",
"segments/ipify",