Repository: JanDeDobbeleer/oh-my-posh
Pull request #3262 (2022-12-28 16:08:54)

between christmas and newyear
- fix(shell): lock template cache correctly
- feat(git): remove stash and worktree count from template
- feat(color): do not clear parent colors

### Prerequisites

- [x] I have read and understood the [contributing guide][CONTRIBUTING.md]
- [x] The commit message follows the [conventional commits][cc] guidelines
- [x] Tests for the changes have been added (for bug fixes/features)
- [x] Docs have been added/updated (for bug fixes/features)

### Description

<!--TemplateBody-->

<!---

Tips:

If you're not comfortable with working with Git, we're working a guide (https://ohmyposh.dev/docs/contributing_git) to help you out.
Oh My Posh advises GitKraken (https://www.gitkraken.com/invite/nQmDPR9D) as your preferred cross platform Git GUI power tool.

-->

[CONTRIBUTING.md]: https://github.com/JanDeDobbeleer/oh-my-posh/blob/main/CONTRIBUTING.md
[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary



Patch:
diff --git a/src/color/plain_writer.go b/src/color/plain_writer.go
index e6090f14..0f57c649 100644
--- a/src/color/plain_writer.go
+++ b/src/color/plain_writer.go
@@ -15,7 +15,6 @@ type PlainWriter struct {
 
 func (a *PlainWriter) SetColors(background, foreground string)       {}
 func (a *PlainWriter) SetParentColors(background, foreground string) {}
-func (a *PlainWriter) ClearParentColors()                            {}
 
 func (a *PlainWriter) Write(background, foreground, text string) {
 	if len(text) == 0 {
diff --git a/src/color/writer.go b/src/color/writer.go
index 92428699..22158890 100644
--- a/src/color/writer.go
+++ b/src/color/writer.go
@@ -15,7 +15,6 @@ type Writer interface {
 	String() (string, int)
 	SetColors(background, foreground string)
 	SetParentColors(background, foreground string)
-	ClearParentColors()
 }
 
 // AnsiWriter writes colorized ANSI strings
@@ -101,10 +100,6 @@ func (a *AnsiWriter) SetParentColors(background, foreground string) {
 	}}, a.ParentColors...)
 }
 
-func (a *AnsiWriter) ClearParentColors() {
-	a.ParentColors = nil
-}
-
 func (a *AnsiWriter) getAnsiFromColorString(colorString string, isBackground bool) AnsiColor {
 	return a.AnsiColors.AnsiColorFromString(colorString, isBackground)
 }
diff --git a/src/engine/block.go b/src/engine/block.go
index 8f2c3d01..42121742 100644
--- a/src/engine/block.go
+++ b/src/engine/block.go
@@ -137,7 +137,6 @@ func (b *Block) RenderSegments() (string, int) {
 		b.renderActiveSegment()
 	}
 	b.writePowerline(true)
-	b.writer.ClearParentColors()
 	return b.writer.String()
 }
 
diff --git a/src/platform/cache.go b/src/platform/cache.go
index 42c66f58..96341bd0 100644
--- a/src/platform/cache.go
+++ b/src/platform/cache.go
@@ -25,13 +25,13 @@ func (c *cacheObject) expired() bool {
 }
 
 type fileCache struct {
-	cache     *concurrentMap
+	cache     *ConcurrentMap
 	cachePath string
 	dirty     bool
 }
 
 func (fc *fileCache) Init(cachePath string) {
-	fc.cache = newConcurrentMap()
+	fc.cache = NewConcurrentMap()
 	fc.cachePath = cachePath
 	cacheFilePath := filepath.Join(fc.cachePath, CacheFile)
 	content, err := os.ReadFile(cacheFilePath)
@@ -48,7 +48,7 @@ func (fc *fileCache) Init(cachePath string) {
 		if co.expired() {
 			continue
 		}
-		fc.cache.set(key, co)
+		fc.cache.Set(key, co)
 	}
 }
 
@@ -56,7 +56,7 @@ func (fc *fileCache) Close() {
 	if !fc.dirty {
 		return
 	}
-	cache := fc.cache.list()
+	cache := fc.cache.List()
 	if dump, err := json.MarshalIndent(cache, "", "    "); err == nil {
 		cacheFilePath := filepath.Join(fc.cachePath, CacheFile)
 		_ = os.WriteFile(cacheFilePath, dump, 0644)
@@ -66,7 +66,7 @@ func (fc *fileCache) Close() {
 // returns the value for the given key as long as
 // the TTL (minutes) is not expired
 func (fc *fileCache) Get(key string) (string, bool) {
-	val, found := fc.cache.get(key)
+	val, found := fc.cache.Get(key)
 	if !found {
 		return "", false
 	}
@@ -78,7 +78,7 @@ func (fc *fileCache) Get(key string) (string, bool) {
 
 // sets the value for the given key with a TTL (minutes)
 func (fc *fileCache) Set(key, value string, ttl int) {
-	fc.cache.set(key, &cacheObject{
+	fc.cache.Set(key, &cacheObject{
 		Value:     value,
 		Timestamp: time.Now().Unix(),
 		TTL:       ttl,
diff --git a/src/platform/concurrent_map.go b/src/platform/concurrent_map.go
index 144def24..5a8c47d4 100644
--- a/src/platform/concurrent_map.go
+++ b/src/platform/concurrent_map.go
@@ -1,30 +1,33 @@
 package platform
 
-type concurrentMap struct {
+import "sync"
+
+type ConcurrentMap struct {
 	values map[string]interface{}
+	sync.RWMutex
 }
 
-func newConcurrentMap() *concurrentMap {
-	return &concurrentMap{
+func NewConcurrentMap() *ConcurrentMap {
+	return &ConcurrentMap{
 		values: make(map[string]interface{}),
 	}
 }
 
-func (c *concurrentMap) set(key string, value interface{}) {
-	lock.Lock()
-	defer lock.Unlock()
+func (c *ConcurrentMap) Set(key string, value interface{}) {
+	c.Lock()
+	defer c.Unlock()
 	c.values[key] = value
 }
 
-func (c *concurrentMap) get(key string) (interface{}, bool) {
-	lock.RLock()
-	defer lock.RUnlock()
+func (c *ConcurrentMap) Get(key string) (interface{}, bool) {
+	c.RLock()
+	defer c.RUnlock()
 	if val, ok := c.values[key]; ok {
 		return val, true
 	}
 	return "", false
 }
 
-func (c *concurrentMap) list() map[string]interface{} {
+func (c *ConcurrentMap) List() map[string]interface{} {
 	return c.values
 }
diff --git a/src/platform/shell.go b/src/platform/shell.go
index 3732d667..1563fa44 100644
--- a/src/platform/shell.go
+++ b/src/platform/shell.go
@@ -42,7 +42,6 @@ func getPID() string {
 }
 
 var (
-	lock          = sync.RWMutex{}
 	TEMPLATECACHE = fmt.Sprintf("template_cache_%s", getPID())
 	TOGGLECACHE   = fmt.Sprintf("toggle_cache_%s", getPID())
 )
@@ -131,8 +130,8 @@ type Connection struct {
 
 type SegmentsCache map[string]interface{}
 
-func (c *SegmentsCache) Contains(key string) bool {
-	_, ok := (*c)[key]
+func (s *SegmentsCache) Contains(key string) bool {
+	_, ok := (*s)[key]
 	return ok
 }
 
@@ -149,15 +148,14 @@ type TemplateCache struct {
 	OS           string
 	WSL          bool
 	Segments     SegmentsCache
+
+	sync.RWMutex
 }
 
 func (t *TemplateCache) AddSegmentData(key string, value interface{}) {
-	lock.Lock()
-	defer lock.Unlock()
-	if t.Segments == nil {
-		t.Segments = make(map[string]interface{})
-	}
+	t.Lock()
 	t.Segments[key] = value
+	t.Unlock()
 }
 
 type Environment interface {
@@ -212,15 +210,15 @@ type Environment interface {
 }
 
 type commandCache struct {
-	commands *concurrentMap
+	commands *ConcurrentMap
 }
 
 func (c *commandCache) set(command, path string) {
-	c.commands.set(command, path)
+	c.commands.Set(command, path)
 }
 
 func (c *commandCache) get(command string) (string, bool) {
-	cacheCommand, found := c.commands.get(command)
+	cacheCommand, found := c.commands.Get(command)
 	if !found {
 		return "", false
 	}
@@ -237,6 +235,8 @@ type Shell struct {
 	fileCache *fileCache
 	tmplCache *TemplateCache
 	networks  []*Connection
+
+	sync.RWMutex
 }
 
 func (env *Shell) Init() {
@@ -251,7 +251,7 @@ func (env *Shell) Init() {
 	env.fileCache.Init(env.CachePath())
 	env.resolveConfigPath()
 	env.cmdCache = &commandCache{
-		commands: newConcurrentMap(),
+		commands: NewConcurrentMap(),
 	}
 }
 
@@ -331,9 +331,7 @@ func (env *Shell) Getenv(key string) string {
 
 func (env *Shell) Pwd() string {
 	defer env.Trace(time.Now(), "Pwd")
-	lock.Lock()
 	defer func() {
-		lock.Unlock()
 		env.Debug("Pwd", env.cwd)
 	}()
 	if env.cwd != "" {
@@ -708,7 +706,11 @@ func (env *Shell) Logs() string {
 }
 
 func (env *Shell) TemplateCache() *TemplateCache {
-	defer env.Trace(time.Now(), "TemplateCache")
+	env.Lock()
+	defer func() {
+		env.Trace(time.Now(), "TemplateCache")
+		env.Unlock()
+	}()
 	if env.tmplCache != nil {
 		return env.tmplCache
 	}
@@ -718,6 +720,7 @@ func (env *Shell) TemplateCache() *TemplateCache {
 		ShellVersion: env.CmdFlags.ShellVersion,
 		Code:         env.ErrorCode(),
 		WSL:          env.IsWsl(),
+		Segments:     make(map[string]interface{}),
 	}
 	tmplCache.Env = make(map[string]string)
 	const separator = "="
@@ -751,8 +754,6 @@ func (env *Shell) TemplateCache() *TemplateCache {
 }
 
 func (env *Shell) DirMatchesOneOf(dir string, regexes []string) (match bool) {
-	lock.Lock()
-	defer lock.Unlock()
 	// sometimes the function panics inside golang, we want to silence that error
 	// and assume that there's no match. Not perfect, but better than crashing
 	// for the time being until we figure out what the actual root cause is
diff --git a/src/segments/git.go b/src/segments/git.go
index 614cc6c2..17e34a22 100644
--- a/src/segments/git.go
+++ b/src/segments/git.go
@@ -108,15 +108,18 @@ type Git struct {
 	UpstreamURL    string
 	RawUpstreamURL string
 	UpstreamGone   bool
-	StashCount     int
-	WorktreeCount  int
 	IsWorkTree     bool
 	RepoName       string
 	IsBare         bool
+
+	// needed for posh-git support
+	poshgit       bool
+	stashCount    int
+	worktreeCount int
 }
 
 func (g *Git) Template() string {
-	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
+	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 }} " //nolint: lll
 }
 
 func (g *Git) Enabled() bool {
@@ -134,10 +137,6 @@ func (g *Git) Enabled() bool {
 		return true
 	}
 
-	if g.props.GetBool(FetchWorktreeCount, false) {
-		g.WorktreeCount = g.getWorktreeContext()
-	}
-
 	if g.hasPoshGitStatus() {
 		return true
 	}
@@ -155,12 +154,22 @@ func (g *Git) Enabled() bool {
 	if len(g.Upstream) != 0 && g.props.GetBool(FetchUpstreamIcon, false) {
 		g.UpstreamIcon = g.getUpstreamIcon()
 	}
-	if g.props.GetBool(FetchStashCount, false) {
-		g.StashCount = g.getStashContext()
-	}
 	return true
 }
 
+func (g *Git) StashCount() int {
+	if g.poshgit || g.stashCount != 0 {
+		return g.stashCount
+	}
+	stashContent := g.FileContents(g.rootDir, "logs/refs/stash")
+	if stashContent == "" {
+		return 0
+	}
+	lines := strings.Split(stashContent, "\n")
+	g.stashCount = len(lines)
+	return g.stashCount
+}
+
 func (g *Git) Kraken() string {
 	root := g.getGitCommandOutput("rev-list", "--max-parents=0", "HEAD")
 	if len(g.RawUpstreamURL) == 0 {
@@ -577,16 +586,10 @@ func (g *Git) setPrettyHEADName() {
 	g.HEAD = fmt.Sprintf("%s%s", g.props.GetString(CommitIcon, "\uF417"), g.ShortHash)
 }
 
-func (g *Git) getStashContext() int {
-	stashContent := g.FileContents(g.rootDir, "logs/refs/stash")
-	if stashContent == "" {
-		return 0
+func (g *Git) WorktreeCount() int {
+	if g.worktreeCount > 0 {
+		return g.worktreeCount
 	}
-	lines := strings.Split(stashContent, "\n")
-	return len(lines)
-}
-
-func (g *Git) getWorktreeContext() int {
 	if !g.env.HasFolder(g.rootDir + "/worktrees") {
 		return 0
 	}
diff --git a/src/segments/posh_git.go b/src/segments/posh_git.go
index 44050014..d5c578e4 100644
--- a/src/segments/posh_git.go
+++ b/src/segments/posh_git.go
@@ -58,7 +58,7 @@ func (g *Git) hasPoshGitStatus() bool {
 	g.Staging = &GitStatus{}
 	g.Staging.parsePoshGitStatus(posh.Index)
 	g.HEAD = g.parsePoshGitHEAD(posh.Branch)
-	g.StashCount = posh.StashCount
+	g.stashCount = posh.StashCount
 	g.Ahead = posh.AheadBy
 	g.Behind = posh.BehindBy
 	g.UpstreamGone = len(posh.Upstream) == 0
@@ -67,6 +67,7 @@ func (g *Git) hasPoshGitStatus() bool {
 	if len(g.Upstream) != 0 && g.props.GetBool(FetchUpstreamIcon, false) {
 		g.UpstreamIcon = g.getUpstreamIcon()
 	}
+	g.poshgit = true
 	return true
 }
 
diff --git a/website/docs/segments/git.mdx b/website/docs/segments/git.mdx
index fdd6f7d2..d368c721 100644
--- a/website/docs/segments/git.mdx
+++ b/website/docs/segments/git.mdx
@@ -70,8 +70,6 @@ You can set the following properties to `true` to enable fetching additional inf
 | Name                   | Type                | Description                                                                                                                                                                                                                                                                                                                           |
 | ---------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
 | `fetch_status`         | `boolean`           | fetch the local changes - defaults to `false`                                                                                                                                                                                                                                                                                         |
-| `fetch_stash_count`    | `boolean`           | fetch stash count - defaults to `false`                                                                                                                                                                                                                                                                                               |
-| `fetch_worktree_count` | `boolean`           | fetch worktree count - defaults to `false`                                                                                                                                                                                                                                                                                            |
 | `fetch_upstream_icon`  | `boolean`           | fetch upstream icon - defaults to `false`                                                                                                                                                                                                                                                                                             |
 | `fetch_bare_info`      | `boolean`           | fetch bare repo info - defaults to `false`                                                                                                                                                                                                                                                                                            |
 | `untracked_modes`      | `map[string]string` | map of repo's where to override the default [untracked files mode][untracked]:<ul><li>`no`</li><li>`normal`</li><li>`all`</li></ul>For example `"untracked_modes": { "/Users/me/repos/repo1": "no" }` - defaults to `normal` for all repo's. If you want to override for all repo's, use `*` to set the mode instead of the repo path |
@@ -119,7 +117,7 @@ You can set the following properties to `true` to enable fetching additional inf
 :::note default template
 
 ```template
-{{ .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 }}
+{{ .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 }}
 ```
 
 :::
@@ -149,15 +147,15 @@ You can set the following properties to `true` to enable fetching additional inf
 
 ### GitStatus
 
-| Name        | Type      | Description                                  |
-| ----------- | --------- | -------------------------------------------- |
-| `.Unmerged` | `int`     | number of unmerged changes                   |
-| `.Deleted`  | `int`     | number of deleted changes                    |
-| `.Added`    | `int`     | number of added changes                      |
-| `.Modified` | `int`     | number of modified changes                   |
-| `.Untracked`| `int`     | number of untracked changes                  |
-| `.Changed`  | `boolean` | if the status contains changes or not        |
-| `.String`   | `string`  | a string representation of the changes above |
+| Name         | Type      | Description                                  |
+| ------------ | --------- | -------------------------------------------- |
+| `.Unmerged`  | `int`     | number of unmerged changes                   |
+| `.Deleted`   | `int`     | number of deleted changes                    |
+| `.Added`     | `int`     | number of added changes                      |
+| `.Modified`  | `int`     | number of modified changes                   |
+| `.Untracked` | `int`     | number of untracked changes                  |
+| `.Changed`   | `boolean` | if the status contains changes or not        |
+| `.String`    | `string`  | a string representation of the changes above |
 
 Local changes use the following syntax:
 


Test patch:
diff --git a/src/engine/migrate_test.go b/src/engine/migrate_test.go
index b8fae4a6..fd5b6ae2 100644
--- a/src/engine/migrate_test.go
+++ b/src/engine/migrate_test.go
@@ -195,7 +195,7 @@ func TestSegmentTemplateMigration(t *testing.T) {
 	}{
 		{
 			Case:     "GIT",
-			Expected: " {{ .HEAD }}{{if .BranchStatus }} {{ .BranchStatus }}{{ end }}{{ if .Working.Changed }} working {{ .Working.String }}{{ end }}{{ if and (.Staging.Changed) (.Working.Changed) }} and{{ end }}{{ if .Staging.Changed }} staged {{ .Staging.String }}{{ end }}{{ if gt .StashCount 0}} stash {{ .StashCount }}{{ end }}{{ if gt .WorktreeCount 0}} worktree {{ .WorktreeCount }}{{ end }} ", //nolint: lll
+			Expected: " {{ .HEAD }}{{if .BranchStatus }} {{ .BranchStatus }}{{ end }}{{ if .Working.Changed }} working {{ .Working.String }}{{ end }}{{ if and (.Staging.Changed) (.Working.Changed) }} and{{ end }}{{ if .Staging.Changed }} staged {{ .Staging.String }}{{ end }} ", //nolint: lll
 			Type:     GIT,
 			Props: properties.Map{
 				"local_working_icon":    " working ",
@@ -207,7 +207,7 @@ func TestSegmentTemplateMigration(t *testing.T) {
 		},
 		{
 			Case:     "GIT - Staging and Working Color",
-			Expected: " {{ .HEAD }}{{if .BranchStatus }} {{ .BranchStatus }}{{ end }}{{ if .Working.Changed }} working <#123456>{{ .Working.String }}</>{{ end }}{{ if and (.Staging.Changed) (.Working.Changed) }} and{{ end }}{{ if .Staging.Changed }} staged <#123456>{{ .Staging.String }}</>{{ end }}{{ if gt .StashCount 0}} stash {{ .StashCount }}{{ end }}{{ if gt .WorktreeCount 0}} worktree {{ .WorktreeCount }}{{ end }} ", //nolint: lll
+			Expected: " {{ .HEAD }}{{if .BranchStatus }} {{ .BranchStatus }}{{ end }}{{ if .Working.Changed }} working <#123456>{{ .Working.String }}</>{{ end }}{{ if and (.Staging.Changed) (.Working.Changed) }} and{{ end }}{{ if .Staging.Changed }} staged <#123456>{{ .Staging.String }}</>{{ end }} ", //nolint: lll
 			Type:     GIT,
 			Props: properties.Map{
 				"local_working_icon":    " working ",
diff --git a/src/segments/git_test.go b/src/segments/git_test.go
index 9f1a8b48..a28ffa1f 100644
--- a/src/segments/git_test.go
+++ b/src/segments/git_test.go
@@ -580,7 +580,7 @@ func TestGetStashContextZeroEntries(t *testing.T) {
 				workingDir: "",
 			},
 		}
-		got := g.getStashContext()
+		got := g.StashCount()
 		assert.Equal(t, tc.Expected, got)
 	}
 }
@@ -760,7 +760,8 @@ func TestGitTemplateString(t *testing.T) {
 						Modified: 1,
 					},
 				},
-				StashCount: 3,
+				stashCount: 3,
+				poshgit:    true,
 			},
 		},
 		{
diff --git a/src/segments/posh_git_test.go b/src/segments/posh_git_test.go
index cc7fbcad..6af9ff4a 100644
--- a/src/segments/posh_git_test.go
+++ b/src/segments/posh_git_test.go
@@ -113,7 +113,7 @@ func TestPoshGitSegment(t *testing.T) {
 				"Branch": "posh-git-json"
 			}
 			`,
-			ExpectedString:  "\ue0a0posh-git-json ↑1 ↓1 \uf044 ~2 | \uf046 -2 \uf692 2",
+			ExpectedString:  "\ue0a0posh-git-json ↑1 ↓1 \uf044 ~2 | \uf046 -2",
 			ExpectedEnabled: true,
 		},
 		{
@@ -140,7 +140,7 @@ func TestPoshGitSegment(t *testing.T) {
 				"Branch": "posh-git-json"
 			}
 			`,
-			ExpectedString:  "\ue0a0posh-git-json ≢ \uf692 2",
+			ExpectedString:  "\ue0a0posh-git-json ≢",
 			ExpectedEnabled: true,
 		},
 		{
@@ -160,7 +160,7 @@ func TestPoshGitSegment(t *testing.T) {
 				"Branch": "posh-git-json"
 			}
 			`,
-			ExpectedString:  "\ue0a0posh-git-json ≢ \uf692 2",
+			ExpectedString:  "\ue0a0posh-git-json ≢",
 			ExpectedEnabled: true,
 		},
 		{