This is an automated email from the ASF dual-hosted git repository.
pcongiusti pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-k.git
The following commit(s) were added to refs/heads/main by this push:
new f0493e227 feat(api): Add Integration Git Branch, Commit, Tag options
f0493e227 is described below
commit f0493e2277b51015ae61bf63628e3b84780071e7
Author: Michal Vavřík <[email protected]>
AuthorDate: Mon Sep 8 14:44:03 2025 +0200
feat(api): Add Integration Git Branch, Commit, Tag options
- Allows to select branch, tag or commit for Git source
- closes: #6135
Changes:
- make the project cloning for the default branch shallow (depth 1 commit)
- add option to checkout Git tag and test it
- add option to checkout Git branch and test it
- add option to checkout Git commit and test it (this is the least
efficient of the 3 as it requires the full project history checkout)
---
.../modules/ROOT/pages/running/build-from-git.adoc | 32 +++++++++
docs/modules/ROOT/partials/apis/camel-k-crds.adoc | 21 ++++++
helm/camel-k/crds/camel-k-crds.yaml | 36 ++++++++++
pkg/apis/camel/v1/build_types.go | 6 ++
pkg/builder/git.go | 49 ++++++++++++-
pkg/builder/git_test.go | 81 ++++++++++++++++++++++
.../applyconfiguration/camel/v1/gitconfigspec.go | 27 ++++++++
pkg/cmd/run.go | 23 +++++-
pkg/cmd/run_test.go | 66 ++++++++++++++++++
.../config/crd/bases/camel.apache.org_builds.yaml | 18 +++++
.../crd/bases/camel.apache.org_integrations.yaml | 9 +++
.../config/crd/bases/camel.apache.org_pipes.yaml | 9 +++
pkg/util/digest/digest.go | 11 ++-
13 files changed, 383 insertions(+), 5 deletions(-)
diff --git a/docs/modules/ROOT/pages/running/build-from-git.adoc
b/docs/modules/ROOT/pages/running/build-from-git.adoc
index 8f5dcd05c..25d043c41 100644
--- a/docs/modules/ROOT/pages/running/build-from-git.adoc
+++ b/docs/modules/ROOT/pages/running/build-from-git.adoc
@@ -33,6 +33,38 @@ The operator will start a Build custom resource, whose goal
is to build and pack
The `kamel` CLI is equipped with a `--git` option that you can use to provide
the project repository.
+== Specifying Branch, Tag or Commit
+
+By default, Camel K clones the repository's default branch (usually `main`).
You can specify a different branch, tag, or specific commit using either CLI
options or YAML configuration:
+
+=== Using CLI Options
+
+```bash
+# Clone specific branch
+kamel run --git https://github.com/michalvavrik/sample.git --git-branch
feature/xyz
+
+# Clone specific tag
+kamel run --git https://github.com/michalvavrik/sample.git --git-tag v1.2.3
+
+# Clone the project and checkout specific commit (full SHA)
+kamel run --git https://github.com/michalvavrik/sample.git --git-commit
f2b9bd064a62263ab53b3bfe6ac2b71e68dba45b
+```
+
+=== Using YAML Configuration
+
+```yaml
+apiVersion: camel.apache.org/v1
+kind: Integration
+metadata:
+ name: sample
+spec:
+ git:
+ url: https://github.com/michalvavrik/sample.git
+ branch: feature/xyz # Use specific branch
+ # tag: v1.2.3 # Or use specific tag
+ # commit: f2b9bd064a62263ab53b3bfe6ac2b71e68dba45b # Or use specific
commit
+```
+
== Rebuild
In order to trigger a rebuild of an Integration you will need to `kamel reset`
or to wipe off the Integration `status` as it normally happens for any other
regular Integration.
diff --git a/docs/modules/ROOT/partials/apis/camel-k-crds.adoc
b/docs/modules/ROOT/partials/apis/camel-k-crds.adoc
index febfe8c40..0f9dd8545 100644
--- a/docs/modules/ROOT/partials/apis/camel-k-crds.adoc
+++ b/docs/modules/ROOT/partials/apis/camel-k-crds.adoc
@@ -2262,6 +2262,27 @@ string
the Kubernetes secret where token is stored
+|`branch` +
+string
+|
+
+
+the git branch to check out
+
+|`tag` +
+string
+|
+
+
+the git tag to check out
+
+|`commit` +
+string
+|
+
+
+the git commit (full SHA) to check out
+
|===
diff --git a/helm/camel-k/crds/camel-k-crds.yaml
b/helm/camel-k/crds/camel-k-crds.yaml
index dfa50d2ba..f9651e49e 100644
--- a/helm/camel-k/crds/camel-k-crds.yaml
+++ b/helm/camel-k/crds/camel-k-crds.yaml
@@ -349,9 +349,18 @@ spec:
description: the configuration of the project to
build on
Git
properties:
+ branch:
+ description: the git branch to check out
+ type: string
+ commit:
+ description: the git commit (full SHA) to check
out
+ type: string
secret:
description: the Kubernetes secret where token
is stored
type: string
+ tag:
+ description: the git tag to check out
+ type: string
url:
description: the URL of the project
type: string
@@ -1262,9 +1271,18 @@ spec:
description: the configuration of the project to
build on
Git
properties:
+ branch:
+ description: the git branch to check out
+ type: string
+ commit:
+ description: the git commit (full SHA) to check
out
+ type: string
secret:
description: the Kubernetes secret where token
is stored
type: string
+ tag:
+ description: the git tag to check out
+ type: string
url:
description: the URL of the project
type: string
@@ -12195,9 +12213,18 @@ spec:
git:
description: the configuration of the project to build on Git
properties:
+ branch:
+ description: the git branch to check out
+ type: string
+ commit:
+ description: the git commit (full SHA) to check out
+ type: string
secret:
description: the Kubernetes secret where token is stored
type: string
+ tag:
+ description: the git tag to check out
+ type: string
url:
description: the URL of the project
type: string
@@ -24168,9 +24195,18 @@ spec:
git:
description: the configuration of the project to build on
Git
properties:
+ branch:
+ description: the git branch to check out
+ type: string
+ commit:
+ description: the git commit (full SHA) to check out
+ type: string
secret:
description: the Kubernetes secret where token is
stored
type: string
+ tag:
+ description: the git tag to check out
+ type: string
url:
description: the URL of the project
type: string
diff --git a/pkg/apis/camel/v1/build_types.go b/pkg/apis/camel/v1/build_types.go
index fd07269ce..05d1cb806 100644
--- a/pkg/apis/camel/v1/build_types.go
+++ b/pkg/apis/camel/v1/build_types.go
@@ -117,6 +117,12 @@ type GitConfigSpec struct {
URL string `json:"url,omitempty"`
// the Kubernetes secret where token is stored
Secret string `json:"secret,omitempty"`
+ // the git branch to check out
+ Branch string `json:"branch,omitempty"`
+ // the git tag to check out
+ Tag string `json:"tag,omitempty"`
+ // the git commit (full SHA) to check out
+ Commit string `json:"commit,omitempty"`
}
// MavenBuildSpec defines the Maven configuration plus additional repositories
to use.
diff --git a/pkg/builder/git.go b/pkg/builder/git.go
index c462162b9..0ee833501 100644
--- a/pkg/builder/git.go
+++ b/pkg/builder/git.go
@@ -18,9 +18,11 @@ limitations under the License.
package builder
import (
+ "errors"
"path/filepath"
git "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/transport/http"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -57,8 +59,31 @@ var Git = gitSteps{
}
func cloneProject(ctx *builderContext) error {
+ depth := 1
+ if ctx.Build.Git.Commit != "" {
+ // only the commit checkout requires full git project history
+ depth = 0
+ }
gitCloneOptions := &git.CloneOptions{
- URL: ctx.Build.Git.URL,
+ URL: ctx.Build.Git.URL,
+ Depth: depth,
+ }
+
+ if ctx.Build.Git.Branch != "" {
+ if ctx.Build.Git.Tag != "" {
+ return errors.New("illegal arguments: cannot specify
both git branch and tag")
+ }
+ if ctx.Build.Git.Commit != "" {
+ return errors.New("illegal arguments: cannot specify
both git branch and commit")
+ }
+ gitCloneOptions.ReferenceName =
plumbing.NewBranchReferenceName(ctx.Build.Git.Branch)
+ gitCloneOptions.SingleBranch = true
+ } else if ctx.Build.Git.Tag != "" {
+ if ctx.Build.Git.Commit != "" {
+ return errors.New("illegal arguments: cannot specify
both git tag and commit")
+ }
+ gitCloneOptions.ReferenceName =
plumbing.NewTagReferenceName(ctx.Build.Git.Tag)
+ gitCloneOptions.SingleBranch = true
}
if ctx.Build.Git.Secret != "" {
@@ -78,7 +103,25 @@ func cloneProject(ctx *builderContext) error {
}
}
- _, err := git.PlainClone(filepath.Join(ctx.Path, "maven"), false,
gitCloneOptions)
+ repo, err := git.PlainClone(filepath.Join(ctx.Path, "maven"), false,
gitCloneOptions)
+
+ if err != nil {
+ return err
+ }
+
+ if ctx.Build.Git.Commit != "" {
+ worktree, err := repo.Worktree()
+ if err != nil {
+ return err
+ }
+ commitHash := plumbing.NewHash(ctx.Build.Git.Commit)
+ err = worktree.Checkout(&git.CheckoutOptions{
+ Hash: commitHash,
+ })
+ if err != nil {
+ return err
+ }
+ }
- return err
+ return nil
}
diff --git a/pkg/builder/git_test.go b/pkg/builder/git_test.go
index 18b051b7a..41b83e4f9 100644
--- a/pkg/builder/git_test.go
+++ b/pkg/builder/git_test.go
@@ -92,3 +92,84 @@ func TestGitPrivateRepoFail(t *testing.T) {
require.Error(t, err)
assert.Contains(t, err.Error(), "no such file or directory")
}
+
+func TestGitCloneBranch(t *testing.T) {
+ tmpGitDir := t.TempDir()
+
+ ctx := &builderContext{
+ C: context.TODO(),
+ Path: tmpGitDir,
+ Build: v1.BuilderTask{
+ Git: &v1.GitConfigSpec{
+ // the project URL for the
https://github.com/squakez/sample.git fork
+ URL:
"https://github.com/michalvavrik/sample.git",
+ // only difference between the main branch and
this branch is the 'this_is_expected_branch' empty file
+ Branch: "feature/branch-checkout-test",
+ },
+ },
+ }
+
+ err := cloneProject(ctx)
+ require.NoError(t, err)
+ f, err := os.Stat(path.Join(tmpGitDir, "maven", "pom.xml"))
+ require.NoError(t, err)
+ assert.Contains(t, f.Name(), "pom.xml")
+
+ f, err = os.Stat(path.Join(tmpGitDir, "maven",
"this_is_expected_branch"))
+ require.NoError(t, err)
+ assert.Contains(t, f.Name(), "this_is_expected_branch")
+}
+
+func TestGitCloneTag(t *testing.T) {
+ tmpGitDir := t.TempDir()
+
+ ctx := &builderContext{
+ C: context.TODO(),
+ Path: tmpGitDir,
+ Build: v1.BuilderTask{
+ Git: &v1.GitConfigSpec{
+ // the project URL for the
https://github.com/squakez/sample.git fork
+ URL:
"https://github.com/michalvavrik/sample.git",
+ // only difference between the main branch and
this tag is the 'this_is_expected_tag' empty file
+ Tag: "v1.2.3",
+ },
+ },
+ }
+
+ err := cloneProject(ctx)
+ require.NoError(t, err)
+ f, err := os.Stat(path.Join(tmpGitDir, "maven", "pom.xml"))
+ require.NoError(t, err)
+ assert.Contains(t, f.Name(), "pom.xml")
+
+ f, err = os.Stat(path.Join(tmpGitDir, "maven", "this_is_expected_tag"))
+ require.NoError(t, err)
+ assert.Contains(t, f.Name(), "this_is_expected_tag")
+}
+
+func TestGitCloneCommit(t *testing.T) {
+ tmpGitDir := t.TempDir()
+
+ ctx := &builderContext{
+ C: context.TODO(),
+ Path: tmpGitDir,
+ Build: v1.BuilderTask{
+ Git: &v1.GitConfigSpec{
+ // the project URL for the
https://github.com/squakez/sample.git fork
+ URL:
"https://github.com/michalvavrik/sample.git",
+ // only difference between the main branch and
this commit is the 'this_is_expected_commit' empty file
+ Commit:
"f2b9bd064a62263ab53b3bfe6ac2b71e68dba45b",
+ },
+ },
+ }
+
+ err := cloneProject(ctx)
+ require.NoError(t, err)
+ f, err := os.Stat(path.Join(tmpGitDir, "maven", "pom.xml"))
+ require.NoError(t, err)
+ assert.Contains(t, f.Name(), "pom.xml")
+
+ f, err = os.Stat(path.Join(tmpGitDir, "maven",
"this_is_expected_commit"))
+ require.NoError(t, err)
+ assert.Contains(t, f.Name(), "this_is_expected_commit")
+}
diff --git a/pkg/client/camel/applyconfiguration/camel/v1/gitconfigspec.go
b/pkg/client/camel/applyconfiguration/camel/v1/gitconfigspec.go
index 7de2b1be6..2f56e4327 100644
--- a/pkg/client/camel/applyconfiguration/camel/v1/gitconfigspec.go
+++ b/pkg/client/camel/applyconfiguration/camel/v1/gitconfigspec.go
@@ -24,6 +24,9 @@ package v1
type GitConfigSpecApplyConfiguration struct {
URL *string `json:"url,omitempty"`
Secret *string `json:"secret,omitempty"`
+ Branch *string `json:"branch,omitempty"`
+ Tag *string `json:"tag,omitempty"`
+ Commit *string `json:"commit,omitempty"`
}
// GitConfigSpecApplyConfiguration constructs a declarative configuration of
the GitConfigSpec type for use with
@@ -47,3 +50,27 @@ func (b *GitConfigSpecApplyConfiguration) WithSecret(value
string) *GitConfigSpe
b.Secret = &value
return b
}
+
+// WithBranch sets the Branch field in the declarative configuration to the
given value
+// and returns the receiver, so that objects can be built by chaining "With"
function invocations.
+// If called multiple times, the Branch field is set to the value of the last
call.
+func (b *GitConfigSpecApplyConfiguration) WithBranch(value string)
*GitConfigSpecApplyConfiguration {
+ b.Branch = &value
+ return b
+}
+
+// WithTag sets the Tag field in the declarative configuration to the given
value
+// and returns the receiver, so that objects can be built by chaining "With"
function invocations.
+// If called multiple times, the Tag field is set to the value of the last
call.
+func (b *GitConfigSpecApplyConfiguration) WithTag(value string)
*GitConfigSpecApplyConfiguration {
+ b.Tag = &value
+ return b
+}
+
+// WithCommit sets the Commit field in the declarative configuration to the
given value
+// and returns the receiver, so that objects can be built by chaining "With"
function invocations.
+// If called multiple times, the Commit field is set to the value of the last
call.
+func (b *GitConfigSpecApplyConfiguration) WithCommit(value string)
*GitConfigSpecApplyConfiguration {
+ b.Commit = &value
+ return b
+}
diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go
index b402601bb..0b9f3d1cf 100644
--- a/pkg/cmd/run.go
+++ b/pkg/cmd/run.go
@@ -117,6 +117,9 @@ func newCmdRun(rootCmdOptions *RootCmdOptions)
(*cobra.Command, *runCmdOptions)
cmd.Flags().String("service-account", "", "The SA to use to run this
Integration")
cmd.Flags().Bool("force", false, "Force creation of integration
regardless of potential misconfiguration.")
cmd.Flags().String("git", "", "A Git repository containing the project
to build.")
+ cmd.Flags().String("git-branch", "", "Git branch to checkout when using
--git option")
+ cmd.Flags().String("git-tag", "", "Git tag to checkout when using --git
option")
+ cmd.Flags().String("git-commit", "", "Git commit (full SHA) to checkout
when using --git option")
cmd.Flags().Bool("save", false, "Save the run parameters into the
default kamel configuration file (kamel-config.yaml)")
// completion support
@@ -138,6 +141,9 @@ type runCmdOptions struct {
IntegrationName string `mapstructure:"name" yaml:",omitempty"`
ContainerImage string `mapstructure:"image" yaml:",omitempty"`
GitRepo string `mapstructure:"git" yaml:",omitempty"`
+ GitBranch string `mapstructure:"git-branch"
yaml:",omitempty"`
+ GitTag string `mapstructure:"git-tag" yaml:",omitempty"`
+ GitCommit string `mapstructure:"git-commit"
yaml:",omitempty"`
Profile string `mapstructure:"profile" yaml:",omitempty"`
IntegrationProfile string `mapstructure:"integration-profile"
yaml:",omitempty"`
OperatorID string `mapstructure:"operator-id"
yaml:",omitempty"`
@@ -565,8 +571,23 @@ func (o *runCmdOptions) createOrUpdateIntegration(cmd
*cobra.Command, c client.C
// Self Managed Integration as the user provided a container
image built externally
o.Traits = append(o.Traits, fmt.Sprintf("container.image=%s",
o.ContainerImage))
} else if o.GitRepo != "" {
+ if o.GitBranch != "" && o.GitTag != "" {
+ err := errors.New("illegal arguments: cannot specify
both git branch and tag")
+ return nil, err
+ }
+ if o.GitBranch != "" && o.GitCommit != "" {
+ err := errors.New("illegal arguments: cannot specify
both git branch and commit")
+ return nil, err
+ }
+ if o.GitTag != "" && o.GitCommit != "" {
+ err := errors.New("illegal arguments: cannot specify
both git tag and commit")
+ return nil, err
+ }
integration.Spec.Git = &v1.GitConfigSpec{
- URL: o.GitRepo,
+ URL: o.GitRepo,
+ Tag: o.GitTag,
+ Branch: o.GitBranch,
+ Commit: o.GitCommit,
}
} else {
return nil, errors.New("you must provide a source, an image or
a git repository parameters")
diff --git a/pkg/cmd/run_test.go b/pkg/cmd/run_test.go
index f343dfa80..6c33b31f8 100644
--- a/pkg/cmd/run_test.go
+++ b/pkg/cmd/run_test.go
@@ -980,3 +980,69 @@ spec:
status: {}
`, output)
}
+
+func TestGitTagIntegration(t *testing.T) {
+ runCmdOptions, runCmd, _ := initializeRunCmdOptionsWithOutput(t)
+ output, err := ExecuteCommand(runCmd, cmdRun, "--git",
"http://my-git/my-org/my-it.git", "--git-tag", "my-tag", "-o", "yaml")
+ assert.Equal(t, "yaml", runCmdOptions.OutputFormat)
+
+ require.NoError(t, err)
+ assert.Equal(t, `apiVersion: camel.apache.org/v1
+kind: Integration
+metadata:
+ annotations:
+ camel.apache.org/operator.id: camel-k
+ creationTimestamp: null
+ name: my-it
+spec:
+ git:
+ tag: my-tag
+ url: http://my-git/my-org/my-it.git
+ traits: {}
+status: {}
+`, output)
+}
+
+func TestGitBranchIntegration(t *testing.T) {
+ runCmdOptions, runCmd, _ := initializeRunCmdOptionsWithOutput(t)
+ output, err := ExecuteCommand(runCmd, cmdRun, "--git",
"http://my-git/my-org/my-it.git", "--git-branch", "my-branch", "-o", "yaml")
+ assert.Equal(t, "yaml", runCmdOptions.OutputFormat)
+
+ require.NoError(t, err)
+ assert.Equal(t, `apiVersion: camel.apache.org/v1
+kind: Integration
+metadata:
+ annotations:
+ camel.apache.org/operator.id: camel-k
+ creationTimestamp: null
+ name: my-it
+spec:
+ git:
+ branch: my-branch
+ url: http://my-git/my-org/my-it.git
+ traits: {}
+status: {}
+`, output)
+}
+
+func TestGitCommitIntegration(t *testing.T) {
+ runCmdOptions, runCmd, _ := initializeRunCmdOptionsWithOutput(t)
+ output, err := ExecuteCommand(runCmd, cmdRun, "--git",
"http://my-git/my-org/my-it.git", "--git-commit", "my-commit", "-o", "yaml")
+ assert.Equal(t, "yaml", runCmdOptions.OutputFormat)
+
+ require.NoError(t, err)
+ assert.Equal(t, `apiVersion: camel.apache.org/v1
+kind: Integration
+metadata:
+ annotations:
+ camel.apache.org/operator.id: camel-k
+ creationTimestamp: null
+ name: my-it
+spec:
+ git:
+ commit: my-commit
+ url: http://my-git/my-org/my-it.git
+ traits: {}
+status: {}
+`, output)
+}
diff --git a/pkg/resources/config/crd/bases/camel.apache.org_builds.yaml
b/pkg/resources/config/crd/bases/camel.apache.org_builds.yaml
index 8a92b9934..f7c0fd64b 100644
--- a/pkg/resources/config/crd/bases/camel.apache.org_builds.yaml
+++ b/pkg/resources/config/crd/bases/camel.apache.org_builds.yaml
@@ -349,9 +349,18 @@ spec:
description: the configuration of the project to
build on
Git
properties:
+ branch:
+ description: the git branch to check out
+ type: string
+ commit:
+ description: the git commit (full SHA) to check
out
+ type: string
secret:
description: the Kubernetes secret where token
is stored
type: string
+ tag:
+ description: the git tag to check out
+ type: string
url:
description: the URL of the project
type: string
@@ -1262,9 +1271,18 @@ spec:
description: the configuration of the project to
build on
Git
properties:
+ branch:
+ description: the git branch to check out
+ type: string
+ commit:
+ description: the git commit (full SHA) to check
out
+ type: string
secret:
description: the Kubernetes secret where token
is stored
type: string
+ tag:
+ description: the git tag to check out
+ type: string
url:
description: the URL of the project
type: string
diff --git a/pkg/resources/config/crd/bases/camel.apache.org_integrations.yaml
b/pkg/resources/config/crd/bases/camel.apache.org_integrations.yaml
index 0db263c37..05d1a21b2 100644
--- a/pkg/resources/config/crd/bases/camel.apache.org_integrations.yaml
+++ b/pkg/resources/config/crd/bases/camel.apache.org_integrations.yaml
@@ -132,9 +132,18 @@ spec:
git:
description: the configuration of the project to build on Git
properties:
+ branch:
+ description: the git branch to check out
+ type: string
+ commit:
+ description: the git commit (full SHA) to check out
+ type: string
secret:
description: the Kubernetes secret where token is stored
type: string
+ tag:
+ description: the git tag to check out
+ type: string
url:
description: the URL of the project
type: string
diff --git a/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml
b/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml
index befc20ee0..3a6d5f3eb 100644
--- a/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml
+++ b/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml
@@ -128,9 +128,18 @@ spec:
git:
description: the configuration of the project to build on
Git
properties:
+ branch:
+ description: the git branch to check out
+ type: string
+ commit:
+ description: the git commit (full SHA) to check out
+ type: string
secret:
description: the Kubernetes secret where token is
stored
type: string
+ tag:
+ description: the git tag to check out
+ type: string
url:
description: the URL of the project
type: string
diff --git a/pkg/util/digest/digest.go b/pkg/util/digest/digest.go
index bfcccd6f2..3f91f95fb 100644
--- a/pkg/util/digest/digest.go
+++ b/pkg/util/digest/digest.go
@@ -112,7 +112,16 @@ func ComputeForIntegration(integration *v1.Integration,
configmapVersions []stri
// Git spec
if integration.Spec.Git != nil {
- if _, err := hash.Write([]byte(integration.Spec.Git.URL + "/" +
integration.Spec.Git.Secret)); err != nil {
+ gitSpec := integration.Spec.Git.URL + "/" +
integration.Spec.Git.Secret
+ switch {
+ case integration.Spec.Git.Tag != "":
+ gitSpec = gitSpec + "/" + integration.Spec.Git.Tag
+ case integration.Spec.Git.Branch != "":
+ gitSpec = gitSpec + "/" + integration.Spec.Git.Branch
+ case integration.Spec.Git.Commit != "":
+ gitSpec = gitSpec + "/" + integration.Spec.Git.Commit
+ }
+ if _, err := hash.Write([]byte(gitSpec)); err != nil {
return "", err
}
}