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{}, PERL: &segments.Perl{},
PHP: &segments.Php{}, PHP: &segments.Php{},
PLASTIC: &segments.Plastic{}, PLASTIC: &segments.Plastic{},
POSHGIT: &segments.PoshGit{},
PROJECT: &segments.Project{}, PROJECT: &segments.Project{},
PYTHON: &segments.Python{}, PYTHON: &segments.Python{},
R: &segments.R{}, R: &segments.R{},

View file

@ -92,34 +92,44 @@ const (
type Git struct { type Git struct {
scm scm
Working *GitStatus Working *GitStatus
Staging *GitStatus Staging *GitStatus
Ahead int Ahead int
Behind int Behind int
HEAD string HEAD string
Ref string Ref string
Hash string Hash string
ShortHash string ShortHash string
BranchStatus string BranchStatus string
Upstream string Upstream string
UpstreamIcon string UpstreamIcon string
UpstreamURL string UpstreamURL string
UpstreamGone bool RawUpstreamURL string
StashCount int UpstreamGone bool
WorktreeCount int StashCount int
IsWorkTree bool WorktreeCount int
RepoName string IsWorkTree bool
RepoName string
} }
func (g *Git) Template() string { 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 { func (g *Git) Enabled() bool {
if !g.shouldDisplay() { if !g.shouldDisplay() {
return false return false
} }
g.RepoName = environment.Base(g.env, g.convertToLinuxPath(g.realDir)) 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) displayStatus := g.props.GetBool(FetchStatus, false)
if displayStatus { if displayStatus {
g.setGitStatus() g.setGitStatus()
@ -130,22 +140,27 @@ func (g *Git) Enabled() bool {
g.Working = &GitStatus{} g.Working = &GitStatus{}
g.Staging = &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() g.UpstreamIcon = g.getUpstreamIcon()
} }
if g.props.GetBool(FetchStashCount, false) { if g.props.GetBool(FetchStashCount, false) {
g.StashCount = g.getStashContext() g.StashCount = g.getStashContext()
} }
if g.props.GetBool(FetchWorktreeCount, false) {
g.WorktreeCount = g.getWorktreeContext()
}
return true return true
} }
func (g *Git) Kraken() string { func (g *Git) Kraken() string {
root := g.getGitCommandOutput("rev-list", "--max-parents=0", "HEAD") root := g.getGitCommandOutput("rev-list", "--max-parents=0", "HEAD")
remote := g.getGitCommandOutput("remote", "get-url", "origin") if len(g.RawUpstreamURL) == 0 {
return fmt.Sprintf("gitkraken://repolink/%s/commit/%s?url=%s", root, g.Hash, url2.QueryEscape(remote)) 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 { func (g *Git) shouldDisplay() bool {
@ -166,12 +181,7 @@ func (g *Git) shouldDisplay() bool {
return false return false
} }
dir := environment.ReplaceHomeDirPrefixWithTilde(g.env, gitdir.Path) // align with template PWD g.setDir(gitdir.Path)
if g.env.GOOS() == environment.WINDOWS {
g.Dir = strings.TrimSuffix(dir, `\.git`)
} else {
g.Dir = strings.TrimSuffix(dir, "/.git")
}
if !gitdir.IsDir { if !gitdir.IsDir {
return g.hasWorktree(gitdir) return g.hasWorktree(gitdir)
@ -184,6 +194,15 @@ func (g *Git) shouldDisplay() bool {
return true 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 { func (g *Git) hasWorktree(gitdir *environment.FileInfo) bool {
g.rootDir = gitdir.Path g.rootDir = gitdir.Path
dirPointer := strings.Trim(g.env.FileContent(gitdir.Path), " \r\n") dirPointer := strings.Trim(g.env.FileContent(gitdir.Path), " \r\n")
@ -264,8 +283,18 @@ func (g *Git) setBranchStatus() {
} }
func (g *Git) getUpstreamIcon() string { func (g *Git) getUpstreamIcon() string {
upstream := regex.ReplaceAllString("/.*", g.Upstream, "") cleanSSHURL := func(url string) string {
g.UpstreamURL = g.getOriginURL(upstream) 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") { if strings.Contains(g.UpstreamURL, "github") {
return g.props.GetString(GithubIcon, "\uF408 ") return g.props.GetString(GithubIcon, "\uF408 ")
} }
@ -539,28 +568,17 @@ func (g *Git) getWorktreeContext() int {
return count return count
} }
func (g *Git) getOriginURL(upstream string) string { func (g *Git) getOriginURL() string {
cleanSSHURL := func(url string) string { upstream := regex.ReplaceAllString("/.*", g.Upstream, "")
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
cfg, err := ini.Load(g.rootDir + "/config") cfg, err := ini.Load(g.rootDir + "/config")
if err != nil { if err != nil {
url = g.getGitCommandOutput("remote", "get-url", upstream) return g.getGitCommandOutput("remote", "get-url", upstream)
return cleanSSHURL(url)
} }
url = cfg.Section("remote \"" + upstream + "\"").Key("url").String() url := cfg.Section("remote \"" + upstream + "\"").Key("url").String()
if url == "" { if len(url) == 0 {
url = g.getGitCommandOutput("remote", "get-url", upstream) url = g.getGitCommandOutput("remote", "get-url", upstream)
} }
return cleanSSHURL(url) return url
} }
func (g *Git) getUntrackedFilesMode() string { 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("HasParentFilePath", ".git").Return(fileInfo, nil)
env.On("PathSeparator").Return("/") env.On("PathSeparator").Return("/")
env.On("Home").Return("/Users/posh") env.On("Home").Return("/Users/posh")
env.On("Getenv", poshGitEnv).Return("")
g := &Git{ g := &Git{
scm: scm{ scm: scm{
env: env, env: env,

View file

@ -1,33 +1,88 @@
package segments package segments
import ( import (
"oh-my-posh/environment" "encoding/json"
"oh-my-posh/properties" "fmt"
"strings" "strings"
) )
type PoshGit struct {
props properties.Properties
env environment.Environment
Status string
}
const ( const (
poshGitEnv = "POSH_GIT_STATUS" poshGitEnv = "POSH_GIT_STATUS"
) )
func (p *PoshGit) Template() string { type poshGit struct {
return " {{ .Status }} " 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 { type poshGitStatus struct {
status := p.env.Getenv(poshGitEnv) Added []string `json:"Added"`
p.Status = strings.TrimSpace(status) Modified []string `json:"Modified"`
return p.Status != "" Deleted []string `json:"Deleted"`
Unmerged []string `json:"Unmerged"`
} }
func (p *PoshGit) Init(props properties.Properties, env environment.Environment) { func (s *GitStatus) parsePoshGitStatus(p *poshGitStatus) {
p.props = props if p == nil {
p.env = env 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 package segments
import ( import (
"oh-my-posh/environment"
"oh-my-posh/mock" "oh-my-posh/mock"
"oh-my-posh/properties" "oh-my-posh/properties"
"testing" "testing"
@ -10,26 +11,229 @@ import (
func TestPoshGitSegment(t *testing.T) { func TestPoshGitSegment(t *testing.T) {
cases := []struct { cases := []struct {
Case string Case string
PoshGitPrompt string PoshGitJSON string
Expected string FetchUpstreamIcon bool
Enabled 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 status",
{Case: "no prompt", PoshGitPrompt: "", Enabled: false}, 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 { for _, tc := range cases {
env := new(mock.MockedEnvironment) env := new(mock.MockedEnvironment)
env.On("Getenv", poshGitEnv).Return(tc.PoshGitPrompt) env.On("Getenv", poshGitEnv).Return(tc.PoshGitJSON)
p := &PoshGit{ env.On("Home").Return("/Users/bill")
env: env, env.On("GOOS").Return(environment.LINUX)
props: &properties.Map{}, 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,
},
},
} }
assert.Equal(t, tc.Enabled, p.Enabled(), tc.Case) if len(tc.Template) == 0 {
if tc.Enabled { tc.Template = g.Template()
assert.Equal(t, tc.Expected, renderTemplate(env, p.Template(), p), tc.Case) }
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.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) { if ($env:POSH_GIT_ENABLED -eq $true) {
# We need to set the status so posh-git can facilitate autocomplete # We need to set the status so posh-git can facilitate autocomplete
$global:GitStatus = Get-GitStatus $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 # the output can be multiline, joining these ensures proper rendering by adding line breaks with `n
$standardOut -join "`n" $standardOut -join "`n"
# remove any posh-git status
$env:POSH_GIT_STATUS = $null
# restore the orignal last exit code # restore the orignal last exit code
$global:LASTEXITCODE = $script:OriginalLastExitCode $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": { "if": {
"properties": { "properties": {

View file

@ -17,15 +17,9 @@ Local changes can also be displayed which uses the following syntax for both the
- `?` untracked - `?` untracked
:::tip :::tip
PowerShell offers support for the `posh-git` module for autocompletion, but it is disabled by default. 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`. Be aware this calls `git status` To enable this, set `$env:POSH_GIT_ENABLED = $true` in your `$PROFILE`. This will also make use of the
twice, so in case you're having a slow repository, it's better to migrate to the [Posh-Git segment][poshgit]. [posh-git][poshgit] output rather than do additional work to get the git status.
:::
:::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].
::: :::
## Sample Configuration ## 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/fossil",
"segments/gcp", "segments/gcp",
"segments/git", "segments/git",
"segments/poshgit",
"segments/golang", "segments/golang",
"segments/haskell", "segments/haskell",
"segments/ipify", "segments/ipify",