Repository: JanDeDobbeleer/oh-my-posh
Pull request #2709 (2022-08-26 19:07:19)

feat(gitversion): add gitversion segment
### 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
This PR adds a new segment to display the SemVer version from GitVersion if the directory/repo you're in has that available.

JanDeDobbeleer: @Lemorz56 I'm currently refactoring this so it can be merged. Stay tuned.
Lemorz56: @JanDeDobbeleer Thanks. Sorry for not replying to your earlier comment, I must have forgotten about fixing that. 

I'm guessing when you are refactoring you will also touch the tests which might be good. I thought they were kinda tricky to validate.
JanDeDobbeleer: @Lemorz56 yes, I' almost done. Will finish up tomorrow and push the changes so you can also have a look.
JanDeDobbeleer: Tested, we can go live.

Patch:
diff --git a/src/engine/segment.go b/src/engine/segment.go
index 362a2da7..f1d6442e 100644
--- a/src/engine/segment.go
+++ b/src/engine/segment.go
@@ -121,6 +121,8 @@ const (
 	GCP SegmentType = "gcp"
 	// GIT represents the git status and information
 	GIT SegmentType = "git"
+	// GITVERSION represents the gitversion information
+	GITVERSION SegmentType = "gitversion"
 	// GOLANG writes which go version is currently active
 	GOLANG SegmentType = "go"
 	// HASKELL segment
@@ -294,6 +296,7 @@ func (segment *Segment) mapSegmentWithWriter(env platform.Environment) error {
 		FOSSIL:        &segments.Fossil{},
 		GCP:           &segments.Gcp{},
 		GIT:           &segments.Git{},
+		GITVERSION:    &segments.GitVersion{},
 		GOLANG:        &segments.Golang{},
 		HASKELL:       &segments.Haskell{},
 		IPIFY:         &segments.IPify{},
diff --git a/src/segments/gitversion.go b/src/segments/gitversion.go
new file mode 100644
index 00000000..3abcd86f
--- /dev/null
+++ b/src/segments/gitversion.go
@@ -0,0 +1,112 @@
+package segments
+
+import (
+	"encoding/json"
+	"errors"
+	"oh-my-posh/platform"
+	"oh-my-posh/properties"
+)
+
+type gitVersion struct {
+	Major                           int    `json:"Major"`
+	Minor                           int    `json:"Minor"`
+	Patch                           int    `json:"Patch"`
+	PreReleaseTag                   string `json:"PreReleaseTag"`
+	PreReleaseTagWithDash           string `json:"PreReleaseTagWithDash"`
+	PreReleaseLabel                 string `json:"PreReleaseLabel"`
+	PreReleaseLabelWithDash         string `json:"PreReleaseLabelWithDash"`
+	PreReleaseNumber                int    `json:"PreReleaseNumber"`
+	WeightedPreReleaseNumber        int    `json:"WeightedPreReleaseNumber"`
+	BuildMetaData                   int    `json:"BuildMetaData"`
+	BuildMetaDataPadded             string `json:"BuildMetaDataPadded"`
+	FullBuildMetaData               string `json:"FullBuildMetaData"`
+	MajorMinorPatch                 string `json:"MajorMinorPatch"`
+	SemVer                          string `json:"SemVer"`
+	LegacySemVer                    string `json:"LegacySemVer"`
+	LegacySemVerPadded              string `json:"LegacySemVerPadded"`
+	AssemblySemVer                  string `json:"AssemblySemVer"`
+	AssemblySemFileVer              string `json:"AssemblySemFileVer"`
+	FullSemVer                      string `json:"FullSemVer"`
+	InformationalVersion            string `json:"InformationalVersion"`
+	BranchName                      string `json:"BranchName"`
+	EscapedBranchName               string `json:"EscapedBranchName"`
+	Sha                             string `json:"Sha"`
+	ShortSha                        string `json:"ShortSha"`
+	NuGetVersionV2                  string `json:"NuGetVersionV2"`
+	NuGetVersion                    string `json:"NuGetVersion"`
+	NuGetPreReleaseTagV2            string `json:"NuGetPreReleaseTagV2"`
+	NuGetPreReleaseTag              string `json:"NuGetPreReleaseTag"`
+	VersionSourceSha                string `json:"VersionSourceSha"`
+	CommitsSinceVersionSource       int    `json:"CommitsSinceVersionSource"`
+	CommitsSinceVersionSourcePadded string `json:"CommitsSinceVersionSourcePadded"`
+	UncommittedChanges              int    `json:"UncommittedChanges"`
+	CommitDate                      string `json:"CommitDate"`
+}
+
+type GitVersion struct {
+	props properties.Properties
+	env   platform.Environment
+
+	gitVersion
+}
+
+func (n *GitVersion) Template() string {
+	return " {{ .MajorMinorPatch }} "
+}
+
+func (n *GitVersion) Enabled() bool {
+	gitversion := "gitversion"
+	if !n.env.HasCommand(gitversion) {
+		return false
+	}
+
+	dir := n.env.Pwd()
+	version, err := n.getCacheValue(dir)
+	// only return on valid cache value
+	if err == nil && len(version.FullSemVer) != 0 {
+		n.gitVersion = *version
+		return true
+	}
+
+	response, err := n.env.RunCommand(gitversion, "-output", "json")
+	if err != nil {
+		return false
+	}
+
+	n.gitVersion = gitVersion{}
+	err = json.Unmarshal([]byte(response), &n.gitVersion)
+	if err != nil {
+		return false
+	}
+
+	cacheTimeout := n.props.GetInt(properties.CacheTimeout, 30)
+	if cacheTimeout > 0 {
+		n.env.Cache().Set(dir, response, cacheTimeout)
+	}
+
+	return true
+}
+
+func (n *GitVersion) getCacheValue(key string) (*gitVersion, error) {
+	var semVer = &gitVersion{}
+	cacheTimeout := n.props.GetInt(properties.CacheTimeout, 30)
+
+	if cacheTimeout <= 0 {
+		return semVer, errors.New("no cache needed")
+	}
+
+	if val, found := n.env.Cache().Get(key); found {
+		err := json.Unmarshal([]byte(val), &semVer)
+		if err != nil {
+			return semVer, err
+		}
+		return semVer, nil
+	}
+	err := errors.New("no data in cache")
+	return semVer, err
+}
+
+func (n *GitVersion) Init(props properties.Properties, env platform.Environment) {
+	n.props = props
+	n.env = env
+}
diff --git a/themes/chips.omp.json b/themes/chips.omp.json
index 98077011..e5822d49 100644
--- a/themes/chips.omp.json
+++ b/themes/chips.omp.json
@@ -212,6 +212,7 @@
             "{{ if empty .Full }}p:c-project-generic-error{{ else }}p:c-project-python{{ end }}"
           ],
           "foreground": "p:c-badge-text",
+          "leading_diamond": "\uE0B6",
           "properties": {
             "display_mode": "context"
           },
diff --git a/themes/schema.json b/themes/schema.json
index 63cf7e03..2e305ac4 100644
--- a/themes/schema.json
+++ b/themes/schema.json
@@ -239,6 +239,7 @@
             "fossil",
             "gcp",
             "git",
+            "gitversion",
             "go",
             "haskell",
             "ipify",
@@ -2855,6 +2856,17 @@
             "title": "GCP Segment",
             "description": "https://ohmyposh.dev/docs/segments/gcp"
           }
+        },
+        {
+          "if": {
+            "properties": {
+              "type": { "const": "gitversion" }
+            }
+          },
+          "then": {
+            "title": "Display GitVersion segment",
+            "description": "https://ohmyposh.dev/docs/segments/gitversion"
+          }
         }
       ]
     }
diff --git a/website/docs/segments/gitversion.mdx b/website/docs/segments/gitversion.mdx
new file mode 100644
index 00000000..eac2cc71
--- /dev/null
+++ b/website/docs/segments/gitversion.mdx
@@ -0,0 +1,53 @@
+---
+id: gitversion
+title: GitVersion
+sidebar_label: GitVersion
+---
+
+## What
+
+Display the [GitVersion][gitversion] version.
+We _strongly_ recommend using [GitVersion Portable][gitversion-portable] for this.
+
+:::caution
+The GitVersion CLI can be a bit slow causing the prompt to feel slow. This why we cache
+the value for 30 minutes by default.
+:::
+
+## Sample Configuration
+
+```json
+{
+  "type": "gitversion",
+  "style": "powerline",
+  "powerline_symbol": "\uE0B0",
+  "foreground": "#ffffff",
+  "background": "#3a579b",
+  "template": " \uF1D2 {{ .MajorMinorPatch }} "
+}
+```
+
+## Properties
+
+| Name            | Type  | Description                                                                                                                                     |
+| --------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
+| `cache_timeout` | `int` | in minutes - How long to wait before fetching new information using the gitversion CLI. Default is 30 minutes, a value of 0 disables the cache. |
+
+## Template ([info][templates])
+
+:::note default template
+
+```template
+{{ .MajorMinorPatch }}
+```
+
+:::
+
+### Properties
+
+You can leverage all variables from the [GitVersion][gitversion] CLI. Have a look at their [documentation][docs] for more information.
+
+[gitversion]: https://github.com/GitTools/GitVersion
+[gitversion-portable]: http://chocolatey.org/packages/GitVersion.Portable
+[templates]: /docs/configuration/templates
+[docs]: https://gitversion.net/docs/reference/variables
diff --git a/website/sidebars.js b/website/sidebars.js
index b84a42d2..665a616f 100644
--- a/website/sidebars.js
+++ b/website/sidebars.js
@@ -73,6 +73,7 @@ module.exports = {
         "segments/fossil",
         "segments/gcp",
         "segments/git",
+        "segments/gitversion",
         "segments/golang",
         "segments/haskell",
         "segments/ipify",


Test patch:
diff --git a/src/segments/gitversion_test.go b/src/segments/gitversion_test.go
new file mode 100644
index 00000000..bb6fc940
--- /dev/null
+++ b/src/segments/gitversion_test.go
@@ -0,0 +1,103 @@
+package segments
+
+import (
+	"errors"
+	"oh-my-posh/mock"
+	"oh-my-posh/properties"
+	"testing"
+
+	"github.com/alecthomas/assert"
+	mock2 "github.com/stretchr/testify/mock"
+)
+
+func TestGitversion(t *testing.T) {
+	cases := []struct {
+		Case            string
+		ExpectedEnabled bool
+		ExpectedString  string
+		Response        string
+		CacheResponse   string
+		CacheError      error
+		HasGitversion   bool
+		Template        string
+		CommandError    error
+		CacheTimeout    int
+	}{
+		{Case: "GitVersion not installed"},
+		{Case: "GitVersion installed, no GitVersion.yml file", HasGitversion: true, Response: "Cannot find the .git directory"},
+		{
+			Case:            "Version",
+			ExpectedEnabled: true,
+			ExpectedString:  "number",
+			HasGitversion:   true,
+			Response:        "{ \"FullSemVer\": \"0.1.0\", \"SemVer\": \"number\" }",
+			Template:        "{{ .SemVer }}",
+		},
+		{
+			Case:            "Cache Version",
+			ExpectedEnabled: true,
+			ExpectedString:  "number2",
+			HasGitversion:   true,
+			CacheResponse:   "{ \"FullSemVer\": \"0.1.2\", \"SemVer\": \"number2\" }",
+			Response:        "{ \"FullSemVer\": \"0.1.0\", \"SemVer\": \"number\" }",
+			Template:        "{{ .SemVer }}",
+		},
+		{
+			Case:            "No Cache enabled",
+			ExpectedEnabled: true,
+			CacheTimeout:    -1,
+			ExpectedString:  "number",
+			HasGitversion:   true,
+			CacheResponse:   "{ \"FullSemVer\": \"0.1.2\", \"SemVer\": \"number2\" }",
+			Response:        "{ \"FullSemVer\": \"0.1.0\", \"SemVer\": \"number\" }",
+			Template:        "{{ .SemVer }}",
+		},
+		{
+			Case:          "Command Error",
+			HasGitversion: true,
+			CommandError:  errors.New("error"),
+		},
+		{
+			Case:            "Bad cache",
+			ExpectedEnabled: true,
+			ExpectedString:  "number",
+			HasGitversion:   true,
+			CacheResponse:   "{{",
+			Response:        "{ \"FullSemVer\": \"0.1.0\", \"SemVer\": \"number\" }",
+			Template:        "{{ .SemVer }}",
+		},
+	}
+
+	for _, tc := range cases {
+		env := new(mock.MockedEnvironment)
+		cache := &mock.MockedCache{}
+
+		env.On("HasCommand", "gitversion").Return(tc.HasGitversion)
+		env.On("Pwd").Return("test-dir")
+		env.On("Cache").Return(cache)
+		cache.On("Get", "test-dir").Return(tc.CacheResponse, len(tc.CacheResponse) != 0)
+		cache.On("Set", mock2.Anything, mock2.Anything, mock2.Anything).Return(tc.Response, true)
+
+		env.On("RunCommand", "gitversion", []string{"-output", "json"}).Return(tc.Response, tc.CommandError)
+
+		if tc.CacheTimeout == 0 {
+			tc.CacheTimeout = 30
+		}
+		gitversion := &GitVersion{
+			env: env,
+			props: properties.Map{
+				properties.CacheTimeout: tc.CacheTimeout,
+			},
+		}
+		if len(tc.Template) == 0 {
+			tc.Template = gitversion.Template()
+		}
+
+		enabled := gitversion.Enabled()
+
+		assert.Equal(t, tc.ExpectedEnabled, enabled, tc.Case)
+		if enabled {
+			assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, gitversion), tc.Case)
+		}
+	}
+}