internal/proxy: expect unprefixed versions in proxy functions For greater consistency across the vulndb module, expect unprefixed semver as inputs to proxy functions, and remove conversions to prefixed semver from packages other than "version" and "proxy". Also updates all proxy function tests to use new test framework. Change-Id: I2509648d8a130159c3af172e0369da888cbb3869 Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/527078 Reviewed-by: Damien Neil <dneil@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index ddc4f3a..a04bf3c 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go
@@ -83,16 +83,30 @@ return b, nil } +func escapePathAndVersion(path, ver string) (ePath, eVersion string, err error) { + ePath, err = module.EscapePath(path) + if err != nil { + return "", "", err + } + if version.IsCommitHash(ver) { + return ePath, ver, nil + } + eVersion, err = module.EscapeVersion("v" + ver) + if err != nil { + return "", "", err + } + if err := module.Check(ePath, eVersion); err != nil { + return "", "", err + } + return ePath, eVersion, err +} + func (c *Client) CanonicalModulePath(path, version string) (_ string, err error) { - escapedPath, err := module.EscapePath(path) + ep, ev, err := escapePathAndVersion(path, version) if err != nil { return "", err } - escapedVersion, err := module.EscapeVersion(version) - if err != nil { - return "", err - } - b, err := c.lookup(fmt.Sprintf("%s/@v/%s.mod", escapedPath, escapedVersion)) + b, err := c.lookup(fmt.Sprintf("%s/@v/%s.mod", ep, ev)) if err != nil { return "", err } @@ -109,11 +123,11 @@ // CanonicalModuleVersion returns the canonical version string (with no leading "v" prefix) // for the given module path and version string. func (c *Client) CanonicalModuleVersion(path, ver string) (_ string, err error) { - escaped, err := module.EscapePath(path) + ep, ev, err := escapePathAndVersion(path, ver) if err != nil { return "", err } - b, err := c.lookup(fmt.Sprintf("%s/@v/%v.info", escaped, ver)) + b, err := c.lookup(fmt.Sprintf("%s/@v/%v.info", ep, ev)) if err != nil { return "", err }
diff --git a/internal/proxy/proxy_test.go b/internal/proxy/proxy_test.go index 08fc05d..c4e0e6a 100644 --- a/internal/proxy/proxy_test.go +++ b/internal/proxy/proxy_test.go
@@ -7,7 +7,6 @@ import ( "errors" "flag" - "fmt" "net/http" "testing" @@ -17,38 +16,33 @@ var realProxy = flag.Bool("proxy", false, "if true, contact the real module proxy and update expected responses") func TestCanonicalModulePath(t *testing.T) { + c, err := NewTestClient(t, *realProxy) + if err != nil { + t.Fatal(err) + } + tcs := []struct { - name string - path string - version string - response string // hard-coded response - want string + name string + path string + version string + want string }{ { - name: "non-canonical", - path: "github.com/golang/vulndb", - version: "v0.0.0-20230522180520-0cbf4ffdb4e7", - response: "module golang.org/x/vulndb", - want: "golang.org/x/vulndb", + name: "non-canonical", + path: "github.com/golang/vulndb", + version: "0.0.0-20230522180520-0cbf4ffdb4e7", + want: "golang.org/x/vulndb", }, { - name: "canonical", - path: "golang.org/x/vulndb", - version: "v0.0.0-20230522180520-0cbf4ffdb4e7", - response: "module golang.org/x/vulndb", - want: "golang.org/x/vulndb", + name: "canonical", + path: "golang.org/x/vulndb", + version: "0.0.0-20230522180520-0cbf4ffdb4e7", + want: "golang.org/x/vulndb", }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - endpoint := fmt.Sprintf("%s/@v/%s.mod", tc.path, tc.version) - c, cleanup := fakeClient(map[string]*response{ - endpoint: { - Body: tc.response, - StatusCode: http.StatusOK, - }}) - t.Cleanup(cleanup) got, err := c.CanonicalModulePath(tc.path, tc.version) if err != nil { t.Fatal(err) @@ -61,38 +55,39 @@ } func TestCanonicalModuleVersion(t *testing.T) { + c, err := NewTestClient(t, *realProxy) + if err != nil { + t.Fatal(err) + } + tcs := []struct { - name string - path string - version string - response string // hard-coded response - want string + name string + path string + version string + want string }{ { - name: "already canonical", - path: "golang.org/x/vulndb", - version: "v0.0.0-20230522180520-0cbf4ffdb4e7", - response: `{"Version":"v0.0.0-20230522180520-0cbf4ffdb4e7"}`, - want: "0.0.0-20230522180520-0cbf4ffdb4e7", + name: "tagged version already canonical", + path: "golang.org/x/vuln", + version: "0.1.0", + want: "0.1.0", }, { - name: "commit hash", - path: "golang.org/x/vulndb", - version: "0cbf4ffdb4e70fce663ec8d59198745b04e7801b", - response: `{"Version":"v0.0.0-20230522180520-0cbf4ffdb4e7"}`, - want: "0.0.0-20230522180520-0cbf4ffdb4e7", + name: "pseudo-version already canonical", + path: "golang.org/x/vulndb", + version: "0.0.0-20230522180520-0cbf4ffdb4e7", + want: "0.0.0-20230522180520-0cbf4ffdb4e7", + }, + { + name: "commit hash", + path: "golang.org/x/vulndb", + version: "0cbf4ffdb4e70fce663ec8d59198745b04e7801b", + want: "0.0.0-20230522180520-0cbf4ffdb4e7", }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - endpoint := fmt.Sprintf("%s/@v/%s.info", tc.path, tc.version) - c, cleanup := fakeClient(map[string]*response{ - endpoint: { - Body: tc.response, - StatusCode: http.StatusOK, - }}) - t.Cleanup(cleanup) got, err := c.CanonicalModuleVersion(tc.path, tc.version) if err != nil { t.Fatal(err) @@ -105,39 +100,35 @@ } func TestVersions(t *testing.T) { + c, err := NewTestClient(t, *realProxy) + if err != nil { + t.Fatal(err) + } + tcs := []struct { - name string - path string - response string // hard-coded response - want []string + name string + path string + want []string }{ { - name: "no tagged versions", - path: "golang.org/x/vulndb", - response: "", - want: nil, + name: "no tagged versions", + path: "golang.org/x/vulndb", + want: nil, }, { - name: "unsorted -> sorted", - path: "golang.org/x/tools", - response: ` -v0.1.4 -v0.9.3 -v0.7.0 -`, - want: []string{"0.1.4", "0.7.0", "0.9.3"}, + name: "tagged versions", + path: "golang.org/x/vuln", + want: []string{ + "0.1.0", + "0.2.0", + "1.0.0", + "1.0.1", + }, }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - endpoint := fmt.Sprintf("%s/@v/list", tc.path) - c, cleanup := fakeClient(map[string]*response{ - endpoint: { - Body: tc.response, - StatusCode: http.StatusOK, - }}) - t.Cleanup(cleanup) got, err := c.Versions(tc.path) if err != nil { t.Fatal(err) @@ -150,27 +141,27 @@ } func TestLatest(t *testing.T) { + c, err := NewTestClient(t, *realProxy) + if err != nil { + t.Fatal(err) + } + tcs := []struct { - path string - response string // hard-coded response - want string + path string + want string }{ { - path: "golang.org/x/vulndb", - response: `{"Version":"v0.0.0-20230522180520-0cbf4ffdb4e7"}`, - want: "0.0.0-20230522180520-0cbf4ffdb4e7", + path: "golang.org/x/vulndb", + want: "0.0.0-20230911193511-c7cbbd05f085", + }, + { + path: "golang.org/x/vuln", + want: "1.0.1", }, } for _, tc := range tcs { t.Run(tc.path, func(t *testing.T) { - endpoint := fmt.Sprintf("%s/@latest", tc.path) - c, cleanup := fakeClient(map[string]*response{ - endpoint: { - Body: tc.response, - StatusCode: http.StatusOK, - }}) - t.Cleanup(cleanup) got, err := c.Latest(tc.path) if err != nil { t.Fatal(err) @@ -235,9 +226,9 @@ } tcs := []struct { - name string - path string - want bool + name string + path string + want bool }{ { name: "exists",
diff --git a/internal/proxy/testdata/proxy/TestCanonicalModulePath.json b/internal/proxy/testdata/proxy/TestCanonicalModulePath.json new file mode 100644 index 0000000..5eb6030 --- /dev/null +++ b/internal/proxy/testdata/proxy/TestCanonicalModulePath.json
@@ -0,0 +1,10 @@ +{ + "github.com/golang/vulndb/@v/v0.0.0-20230522180520-0cbf4ffdb4e7.mod": { + "body": "module golang.org/x/vulndb\n\ngo 1.18\n\nrequire (\n\tgolang.org/x/vuln v0.0.0-20230217204342-b91abcc5ae3c\n\tgolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect\n)\n\nrequire (\n\tcloud.google.com/go/errorreporting v0.1.0\n\tcloud.google.com/go/firestore v1.6.1\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go v1.0.0\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.26.0\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.0.0\n\tgithub.com/client9/misspell v0.3.4\n\tgithub.com/go-git/go-billy/v5 v5.3.1\n\tgithub.com/go-git/go-git/v5 v5.4.2\n\tgithub.com/google/go-cmp v0.5.8\n\tgithub.com/google/go-github/v41 v41.0.0\n\tgithub.com/google/safehtml v0.0.2\n\tgithub.com/jba/templatecheck v0.6.0\n\tgithub.com/shurcooL/githubv4 v0.0.0-20220115235240-a14260e6f8a2\n\tgo.opentelemetry.io/otel v1.4.0\n\tgo.opentelemetry.io/otel/sdk v1.4.0\n\tgolang.org/x/exp v0.0.0-20220722155223-a9213eeb770e\n\tgolang.org/x/exp/event v0.0.0-20220218215828-6cf2b201936e\n\tgolang.org/x/mod v0.10.0\n\tgolang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8\n\tgolang.org/x/sync v0.2.0\n\tgolang.org/x/time v0.0.0-20191024005414-555d28b269f0\n\tgolang.org/x/tools v0.9.1\n\tgoogle.golang.org/api v0.70.0\n\tgoogle.golang.org/grpc v1.44.0\n\tgopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c\n\thonnef.co/go/tools v0.2.2\n\tmvdan.cc/unparam v0.0.0-20220926085101-66de63301820\n)\n\nrequire (\n\tcloud.google.com/go v0.100.2 // indirect\n\tcloud.google.com/go/compute v1.3.0 // indirect\n\tcloud.google.com/go/monitoring v1.2.0 // indirect\n\tcloud.google.com/go/trace v1.0.0 // indirect\n\tgithub.com/BurntSushi/toml v0.3.1 // indirect\n\tgithub.com/Microsoft/go-winio v0.4.16 // indirect\n\tgithub.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect\n\tgithub.com/acomagu/bufpipe v1.0.3 // indirect\n\tgithub.com/emirpasic/gods v1.12.0 // indirect\n\tgithub.com/go-git/gcfg v1.5.0 // indirect\n\tgithub.com/go-logr/logr v1.2.2 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect\n\tgithub.com/golang/protobuf v1.5.2 // indirect\n\tgithub.com/google/go-querystring v1.1.0 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.1.1 // indirect\n\tgithub.com/imdario/mergo v0.3.12 // indirect\n\tgithub.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect\n\tgithub.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect\n\tgithub.com/mitchellh/go-homedir v1.1.0 // indirect\n\tgithub.com/sergi/go-diff v1.1.0 // indirect\n\tgithub.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a // indirect\n\tgithub.com/xanzy/ssh-agent v0.3.0 // indirect\n\tgo.opencensus.io v0.23.0 // indirect\n\tgo.opentelemetry.io/otel/internal/metric v0.27.0 // indirect\n\tgo.opentelemetry.io/otel/metric v0.27.0 // indirect\n\tgo.opentelemetry.io/otel/sdk/export/metric v0.26.0 // indirect\n\tgo.opentelemetry.io/otel/sdk/metric v0.26.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.4.0 // indirect\n\tgolang.org/x/crypto v0.1.0 // indirect\n\tgolang.org/x/net v0.10.0 // indirect\n\tgolang.org/x/sys v0.8.0 // indirect\n\tgolang.org/x/text v0.9.0 // indirect\n\tgoogle.golang.org/appengine v1.6.7 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf // indirect\n\tgoogle.golang.org/protobuf v1.27.1 // indirect\n\tgopkg.in/warnings.v0 v0.1.2 // indirect\n)\n", + "status_code": 200 + }, + "golang.org/x/vulndb/@v/v0.0.0-20230522180520-0cbf4ffdb4e7.mod": { + "body": "module golang.org/x/vulndb\n\ngo 1.18\n\nrequire (\n\tgolang.org/x/vuln v0.0.0-20230217204342-b91abcc5ae3c\n\tgolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect\n)\n\nrequire (\n\tcloud.google.com/go/errorreporting v0.1.0\n\tcloud.google.com/go/firestore v1.6.1\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go v1.0.0\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.26.0\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.0.0\n\tgithub.com/client9/misspell v0.3.4\n\tgithub.com/go-git/go-billy/v5 v5.3.1\n\tgithub.com/go-git/go-git/v5 v5.4.2\n\tgithub.com/google/go-cmp v0.5.8\n\tgithub.com/google/go-github/v41 v41.0.0\n\tgithub.com/google/safehtml v0.0.2\n\tgithub.com/jba/templatecheck v0.6.0\n\tgithub.com/shurcooL/githubv4 v0.0.0-20220115235240-a14260e6f8a2\n\tgo.opentelemetry.io/otel v1.4.0\n\tgo.opentelemetry.io/otel/sdk v1.4.0\n\tgolang.org/x/exp v0.0.0-20220722155223-a9213eeb770e\n\tgolang.org/x/exp/event v0.0.0-20220218215828-6cf2b201936e\n\tgolang.org/x/mod v0.10.0\n\tgolang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8\n\tgolang.org/x/sync v0.2.0\n\tgolang.org/x/time v0.0.0-20191024005414-555d28b269f0\n\tgolang.org/x/tools v0.9.1\n\tgoogle.golang.org/api v0.70.0\n\tgoogle.golang.org/grpc v1.44.0\n\tgopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c\n\thonnef.co/go/tools v0.2.2\n\tmvdan.cc/unparam v0.0.0-20220926085101-66de63301820\n)\n\nrequire (\n\tcloud.google.com/go v0.100.2 // indirect\n\tcloud.google.com/go/compute v1.3.0 // indirect\n\tcloud.google.com/go/monitoring v1.2.0 // indirect\n\tcloud.google.com/go/trace v1.0.0 // indirect\n\tgithub.com/BurntSushi/toml v0.3.1 // indirect\n\tgithub.com/Microsoft/go-winio v0.4.16 // indirect\n\tgithub.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect\n\tgithub.com/acomagu/bufpipe v1.0.3 // indirect\n\tgithub.com/emirpasic/gods v1.12.0 // indirect\n\tgithub.com/go-git/gcfg v1.5.0 // indirect\n\tgithub.com/go-logr/logr v1.2.2 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect\n\tgithub.com/golang/protobuf v1.5.2 // indirect\n\tgithub.com/google/go-querystring v1.1.0 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.1.1 // indirect\n\tgithub.com/imdario/mergo v0.3.12 // indirect\n\tgithub.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect\n\tgithub.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect\n\tgithub.com/mitchellh/go-homedir v1.1.0 // indirect\n\tgithub.com/sergi/go-diff v1.1.0 // indirect\n\tgithub.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a // indirect\n\tgithub.com/xanzy/ssh-agent v0.3.0 // indirect\n\tgo.opencensus.io v0.23.0 // indirect\n\tgo.opentelemetry.io/otel/internal/metric v0.27.0 // indirect\n\tgo.opentelemetry.io/otel/metric v0.27.0 // indirect\n\tgo.opentelemetry.io/otel/sdk/export/metric v0.26.0 // indirect\n\tgo.opentelemetry.io/otel/sdk/metric v0.26.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.4.0 // indirect\n\tgolang.org/x/crypto v0.1.0 // indirect\n\tgolang.org/x/net v0.10.0 // indirect\n\tgolang.org/x/sys v0.8.0 // indirect\n\tgolang.org/x/text v0.9.0 // indirect\n\tgoogle.golang.org/appengine v1.6.7 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf // indirect\n\tgoogle.golang.org/protobuf v1.27.1 // indirect\n\tgopkg.in/warnings.v0 v0.1.2 // indirect\n)\n", + "status_code": 200 + } +} \ No newline at end of file
diff --git a/internal/proxy/testdata/proxy/TestCanonicalModuleVersion.json b/internal/proxy/testdata/proxy/TestCanonicalModuleVersion.json new file mode 100644 index 0000000..79527cc --- /dev/null +++ b/internal/proxy/testdata/proxy/TestCanonicalModuleVersion.json
@@ -0,0 +1,14 @@ +{ + "golang.org/x/vuln/@v/v0.1.0.info": { + "body": "{\"Version\":\"v0.1.0\",\"Time\":\"2023-04-24T18:46:43Z\",\"Origin\":{\"VCS\":\"git\",\"URL\":\"https://go.googlesource.com/vuln\",\"Ref\":\"refs/tags/v0.1.0\",\"Hash\":\"b43f5afc876383b2adc0ec0d3ff1998fe58eeda0\"}}", + "status_code": 200 + }, + "golang.org/x/vulndb/@v/0cbf4ffdb4e70fce663ec8d59198745b04e7801b.info": { + "body": "{\"Version\":\"v0.0.0-20230522180520-0cbf4ffdb4e7\",\"Time\":\"2023-05-22T18:05:20Z\",\"Origin\":{\"VCS\":\"git\",\"URL\":\"https://go.googlesource.com/vulndb\",\"Hash\":\"0cbf4ffdb4e70fce663ec8d59198745b04e7801b\"}}", + "status_code": 200 + }, + "golang.org/x/vulndb/@v/v0.0.0-20230522180520-0cbf4ffdb4e7.info": { + "body": "{\"Version\":\"v0.0.0-20230522180520-0cbf4ffdb4e7\",\"Time\":\"2023-05-22T18:05:20Z\",\"Origin\":{\"VCS\":\"git\",\"URL\":\"https://go.googlesource.com/vulndb\",\"Hash\":\"0cbf4ffdb4e70fce663ec8d59198745b04e7801b\"}}", + "status_code": 200 + } +} \ No newline at end of file
diff --git a/internal/proxy/testdata/proxy/TestLatest.json b/internal/proxy/testdata/proxy/TestLatest.json new file mode 100644 index 0000000..c3ac0d8 --- /dev/null +++ b/internal/proxy/testdata/proxy/TestLatest.json
@@ -0,0 +1,10 @@ +{ + "golang.org/x/vuln/@latest": { + "body": "{\"Version\":\"v1.0.1\",\"Time\":\"2023-08-17T17:30:53Z\",\"Origin\":{\"VCS\":\"git\",\"URL\":\"https://go.googlesource.com/vuln\",\"Ref\":\"refs/tags/v1.0.1\",\"Hash\":\"da4b74a5408a0116e9a2dde953659a7b0956dc56\"}}", + "status_code": 200 + }, + "golang.org/x/vulndb/@latest": { + "body": "{\"Version\":\"v0.0.0-20230911193511-c7cbbd05f085\",\"Time\":\"2023-09-11T19:35:11Z\",\"Origin\":{\"VCS\":\"git\",\"URL\":\"https://go.googlesource.com/vulndb\",\"Hash\":\"c7cbbd05f085e3127d7d29048cdfd474d42964f0\"}}", + "status_code": 200 + } +} \ No newline at end of file
diff --git a/internal/proxy/testdata/proxy/TestVersions.json b/internal/proxy/testdata/proxy/TestVersions.json new file mode 100644 index 0000000..6085bd7 --- /dev/null +++ b/internal/proxy/testdata/proxy/TestVersions.json
@@ -0,0 +1,9 @@ +{ + "golang.org/x/vuln/@v/list": { + "body": "v1.0.0\nv0.1.0\nv1.0.1\nv0.2.0\n", + "status_code": 200 + }, + "golang.org/x/vulndb/@v/list": { + "status_code": 200 + } +} \ No newline at end of file
diff --git a/internal/report/fix.go b/internal/report/fix.go index 56f90a7..e20dcce 100644 --- a/internal/report/fix.go +++ b/internal/report/fix.go
@@ -15,8 +15,6 @@ "golang.org/x/vulndb/internal/version" ) -var commitHashRegex = regexp.MustCompile(`^[a-f0-9]+$`) - func (r *Report) Fix(pc *proxy.Client) { for _, ref := range r.References { ref.URL = fixURL(ref.URL) @@ -41,8 +39,8 @@ if v == "" { return "" } - if commitHashRegex.MatchString(v) { - if c, err := pc.CanonicalModuleVersion(m.Module, v); err == nil { + if version.IsCommitHash(v) { + if c, err := pc.CanonicalModuleVersion(m.Module, v); err == nil { // no error v = c } }
diff --git a/internal/report/lint.go b/internal/report/lint.go index 4644dc3..20c8f47 100644 --- a/internal/report/lint.go +++ b/internal/report/lint.go
@@ -28,11 +28,7 @@ if v == "" { return nil } - vv := "v" + v - if err := module.Check(modPath, vv); err != nil { - return err - } - canonicalPath, err := pc.CanonicalModulePath(modPath, vv) + canonicalPath, err := pc.CanonicalModulePath(modPath, v) if err != nil { return err }
diff --git a/internal/version/semver.go b/internal/version/semver.go index fbb5437..6eb2ad2 100644 --- a/internal/version/semver.go +++ b/internal/version/semver.go
@@ -7,6 +7,7 @@ package version import ( + "regexp" "strings" "golang.org/x/mod/semver" @@ -46,3 +47,9 @@ v = strings.TrimPrefix(v, "go") return v } + +var commitHashRegex = regexp.MustCompile(`^[a-f0-9]+$`) + +func IsCommitHash(v string) bool { + return commitHashRegex.MatchString(v) +}