Repository: JanDeDobbeleer/oh-my-posh
Pull request #1428 (2021-12-13 21:36:32)

feat(go): parse go.mod file for version
### Prerequisites

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

### Description

If a go.mod file is present, parse that file for the go version.

<!---

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.

-->

[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary



Patch:
diff --git a/docs/docs/segment-golang.md b/docs/docs/segment-golang.md
index 1aacf4b3..d32155b0 100644
--- a/docs/docs/segment-golang.md
+++ b/docs/docs/segment-golang.md
@@ -34,6 +34,7 @@ Display the currently active golang version.
   - `files`: the segment is only displayed when `*.go` or `go.mod` files are present (default)
 - template: `string` - A go [text/template][go-text-template] template extended with [sprig][sprig] utilizing the
 properties below. Defaults to `{{ .Full }}`
+- parse_mod_file: `boolean`: parse the go.mod file instead of calling `go version`
 
 ## Template Properties
 
diff --git a/src/go.mod b/src/go.mod
index 6fae79ac..2221c17d 100644
--- a/src/go.mod
+++ b/src/go.mod
@@ -35,7 +35,10 @@ require (
 	howett.net/plist v0.0.0-20201203080718-1454fab16a06 // indirect
 )
 
-require gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
+require (
+	golang.org/x/mod v0.5.1
+	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
+)
 
 require (
 	github.com/Masterminds/goutils v1.1.1 // indirect
@@ -55,6 +58,7 @@ require (
 	github.com/spf13/cast v1.4.1 // indirect
 	github.com/tklauser/numcpus v0.3.0 // indirect
 	github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
+	golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 )
 
diff --git a/src/go.sum b/src/go.sum
index aaf144ea..4df2de00 100644
--- a/src/go.sum
+++ b/src/go.sum
@@ -141,18 +141,23 @@ github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUA
 github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI=
 golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
 golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
+golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
 golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -179,6 +184,9 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
diff --git a/src/segment_golang.go b/src/segment_golang.go
index c8a620a5..d608dee5 100644
--- a/src/segment_golang.go
+++ b/src/segment_golang.go
@@ -1,9 +1,17 @@
 package main
 
+import (
+	"golang.org/x/mod/modfile"
+)
+
 type golang struct {
 	language
 }
 
+const (
+	ParseModFile Property = "parse_mod_file"
+)
+
 func (g *golang) string() string {
 	return g.language.string()
 }
@@ -14,6 +22,10 @@ func (g *golang) init(props properties, env environmentInfo) {
 		props:      props,
 		extensions: []string{"*.go", "go.mod"},
 		commands: []*cmd{
+			{
+				regex:      `(?P<version>((?P<major>[0-9]+).(?P<minor>[0-9]+)(.(?P<patch>[0-9]+))?))`,
+				getVersion: g.getVersion,
+			},
 			{
 				executable: "go",
 				args:       []string{"version"},
@@ -24,6 +36,22 @@ func (g *golang) init(props properties, env environmentInfo) {
 	}
 }
 
+func (g *golang) getVersion() (string, error) {
+	if !g.props.getBool(ParseModFile, false) {
+		return "", nil
+	}
+	gomod, err := g.language.env.hasParentFilePath("go.mod")
+	if err != nil {
+		return "", nil
+	}
+	contents := g.language.env.getFileContent(gomod.path)
+	file, err := modfile.Parse(gomod.path, []byte(contents), nil)
+	if err != nil {
+		return "", err
+	}
+	return file.Go.Version, nil
+}
+
 func (g *golang) enabled() bool {
 	return g.language.enabled()
 }
diff --git a/themes/schema.json b/themes/schema.json
index a47853f2..bbca05d0 100644
--- a/themes/schema.json
+++ b/themes/schema.json
@@ -640,6 +640,12 @@
                   "fetch_version": {
                     "$ref": "#/definitions/fetch_version"
                   },
+                  "parse_mod_file": {
+                    "type": "boolean",
+                    "title": "Parse go.mod file",
+                    "description": "Parse go.mod file instead of calling out to go to improve performance.",
+                    "default": false
+                  },
                   "display_mode": {
                     "$ref": "#/definitions/display_mode"
                   },


Test patch:
diff --git a/src/segment_golang_test.go b/src/segment_golang_test.go
index e95976dd..d1e93371 100644
--- a/src/segment_golang_test.go
+++ b/src/segment_golang_test.go
@@ -1,7 +1,9 @@
 package main
 
 import (
+	"errors"
 	"fmt"
+	"io/ioutil"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -29,12 +31,25 @@ func getMockedLanguageEnv(params *mockedLanguageParams) (*MockedEnvironment, pro
 
 func TestGolang(t *testing.T) {
 	cases := []struct {
-		Case           string
-		ExpectedString string
-		Version        string
+		Case                  string
+		ExpectedString        string
+		Version               string
+		ParseModFile          bool
+		HasModFileInParentDir bool
+		InvalidModfile        bool
 	}{
 		{Case: "Go 1.15", ExpectedString: "1.15.8", Version: "go version go1.15.8 darwin/amd64"},
 		{Case: "Go 1.16", ExpectedString: "1.16", Version: "go version go1.16 darwin/amd64"},
+		{Case: "go.mod 1.17", ParseModFile: true, HasModFileInParentDir: true, ExpectedString: "1.17"},
+		{Case: "no go.mod file fallback", ParseModFile: true, ExpectedString: "1.16", Version: "go version go1.16 darwin/amd64"},
+		{
+			Case:                  "invalid go.mod file fallback",
+			ParseModFile:          true,
+			HasModFileInParentDir: true,
+			InvalidModfile:        true,
+			ExpectedString:        "./go.mod:1: unknown directive: invalid",
+			Version:               "go version go1.16 darwin/amd64",
+		},
 	}
 	for _, tc := range cases {
 		params := &mockedLanguageParams{
@@ -44,6 +59,27 @@ func TestGolang(t *testing.T) {
 			extension:     "*.go",
 		}
 		env, props := getMockedLanguageEnv(params)
+		if tc.ParseModFile {
+			props[ParseModFile] = tc.ParseModFile
+			fileInfo := &fileInfo{
+				path:         "./go.mod",
+				parentFolder: "./",
+				isDir:        false,
+			}
+			var err error
+			if !tc.HasModFileInParentDir {
+				err = errors.New("no match")
+			}
+			env.On("hasParentFilePath", "go.mod").Return(fileInfo, err)
+			var content string
+			if tc.InvalidModfile {
+				content = "invalid go.mod file"
+			} else {
+				tmp, _ := ioutil.ReadFile(fileInfo.path)
+				content = string(tmp)
+			}
+			env.On("getFileContent", fileInfo.path).Return(content)
+		}
 		g := &golang{}
 		g.init(props, env)
 		assert.True(t, g.enabled(), fmt.Sprintf("Failed in case: %s", tc.Case))