From ffe1b508086b03787285872365546097b8ad6eeb Mon Sep 17 00:00:00 2001 From: Christoph Ostarek Date: Tue, 3 Sep 2024 14:40:23 +0200 Subject: [PATCH] tools: introduce git-change-exec tool This new tool detects if in your git tree changed files (compared to master branch and local-only changed files) and uses this information to run only the specified actions. Here it is used to run pillar's go-tests only if something changed there, same for this tool itself and the get-deps tool. Signed-off-by: Christoph Ostarek --- .github/workflows/go-tests.yml | 3 + .markdownlint.yaml | 3 + Makefile | 5 +- tools/git-change-exec/.gitignore | 1 + tools/git-change-exec/Makefile | 14 ++ tools/git-change-exec/README.md | 31 +++ tools/git-change-exec/actions-lint.go | 206 +++++++++++++++++ tools/git-change-exec/actions-test.go | 87 ++++++++ tools/git-change-exec/actions.go | 50 +++++ tools/git-change-exec/actions_test.go | 15 ++ tools/git-change-exec/go.mod | 34 +++ tools/git-change-exec/go.sum | 153 +++++++++++++ tools/git-change-exec/main.go | 307 ++++++++++++++++++++++++++ 13 files changed, 907 insertions(+), 2 deletions(-) create mode 100644 tools/git-change-exec/.gitignore create mode 100644 tools/git-change-exec/Makefile create mode 100644 tools/git-change-exec/README.md create mode 100644 tools/git-change-exec/actions-lint.go create mode 100644 tools/git-change-exec/actions-test.go create mode 100644 tools/git-change-exec/actions.go create mode 100644 tools/git-change-exec/actions_test.go create mode 100644 tools/git-change-exec/go.mod create mode 100644 tools/git-change-exec/go.sum create mode 100644 tools/git-change-exec/main.go diff --git a/.github/workflows/go-tests.yml b/.github/workflows/go-tests.yml index ebda86ef95..6855a50e8b 100644 --- a/.github/workflows/go-tests.yml +++ b/.github/workflows/go-tests.yml @@ -1,3 +1,6 @@ +# Copyright (c) 2024 Zededa, Inc. +# SPDX-License-Identifier: Apache-2.0 + --- name: Go Tests on: # yamllint disable-line rule:truthy diff --git a/.markdownlint.yaml b/.markdownlint.yaml index 41121bf24f..0069ad3844 100644 --- a/.markdownlint.yaml +++ b/.markdownlint.yaml @@ -1,3 +1,6 @@ +# Copyright (c) 2024 Zededa, Inc. +# SPDX-License-Identifier: Apache-2.0 + --- default: true line-length: false diff --git a/Makefile b/Makefile index 1a1e0e80bc..5430f28b8a 100644 --- a/Makefile +++ b/Makefile @@ -428,10 +428,11 @@ currentversion: test: $(LINUXKIT) test-images-patches | $(DIST) @echo Running tests on $(GOMODULE) - make -C pkg/pillar test + make -C tools/git-change-exec + ./tools/git-change-exec/git-change-exec test + touch pkg/pillar/results.json pkg/pillar/results.xml cp pkg/pillar/results.json $(DIST)/ cp pkg/pillar/results.xml $(DIST)/ - make -C eve-tools/bpftrace-compiler test $(QUIET): $@: Succeeded test-profiling: diff --git a/tools/git-change-exec/.gitignore b/tools/git-change-exec/.gitignore new file mode 100644 index 0000000000..bf40f2f47e --- /dev/null +++ b/tools/git-change-exec/.gitignore @@ -0,0 +1 @@ +git-change-exec diff --git a/tools/git-change-exec/Makefile b/tools/git-change-exec/Makefile new file mode 100644 index 0000000000..dd6aab7b16 --- /dev/null +++ b/tools/git-change-exec/Makefile @@ -0,0 +1,14 @@ +# Copyright (c) 2024 Zededa, Inc. +# SPDX-License-Identifier: Apache-2.0 + +SOURCES=actions.go actions-lint.go actions-test.go actions_test.go main.go +git-change-exec: $(SOURCES) + go build + +.PHONY: clean + +clean: + rm -fv git-change-exec + +test:$(SOURCES) + go test diff --git a/tools/git-change-exec/README.md b/tools/git-change-exec/README.md new file mode 100644 index 0000000000..62179dc867 --- /dev/null +++ b/tools/git-change-exec/README.md @@ -0,0 +1,31 @@ +# git-change-exec + +This new tool detects if in your git tree files changed +compared to: + +* master branch +* stable branches + +also local-only files are considered. + +Here it is used to run pillar's go-tests only if something +changed there, same for this tool itself and the get-deps tool. + +## Add new Action + +Open `actions.go` add new implementation of `action` `interface` and +add it to the `actions` array. + +Run `git-change-exec -d` to see if your action gets triggered without +running. + +## Example output + +```bash +2024/09/04 09:49:17 --- running gitChangeExecTest ... +=== RUN TestId +--- PASS: TestId (0.00s) +PASS +ok git-change-exec 0.004s +2024/09/04 09:49:20 --- running gitChangeExecTest done +``` diff --git a/tools/git-change-exec/actions-lint.go b/tools/git-change-exec/actions-lint.go new file mode 100644 index 0000000000..4f6cfaa519 --- /dev/null +++ b/tools/git-change-exec/actions-lint.go @@ -0,0 +1,206 @@ +// Copyright (c) 2024 Zededa, Inc. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + "github.com/go-git/go-git/v5/config" + "io" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +type lintSpdx struct { + extsMap map[string]func(path string) + pathsToFix []string + ownPath string + organization string +} + +func (s *lintSpdx) dontfix(path string) { + log.Printf("Cannot fix %s ...\n", path) +} + +func (s *lintSpdx) copyright(commentIndicator string) []string { + copyrightLines := []string{ + fmt.Sprintf("%s Copyright (c) %d %s, Inc.\n", commentIndicator, time.Now().Year(), s.organization), + fmt.Sprintf("%s SPDX-License-Identifier: Apache-2.0\n\n", commentIndicator), + } + + return copyrightLines +} + +func (s *lintSpdx) yamlfix(path string) { + log.Printf("Fixing %s ...\n", path) + prepend(path, s.copyright("#")) +} + +func (s *lintSpdx) gofix(path string) { + log.Printf("Fixing %s ...\n", path) + prepend(path, s.copyright("//")) +} + +func (s *lintSpdx) dockerfilefix(path string) { + log.Printf("Fixing %s ...\n", path) + prepend(path, s.copyright("#")) +} + +func prepend(path string, license []string) { + backupFh, err := os.CreateTemp("/var/tmp", "git-change-exec-spdx-fix") + if err != nil { + log.Fatalf("could not create temp file: %v", err) + } + backupPath := backupFh.Name() + defer os.Remove(backupPath) + + for _, line := range license { + fmt.Fprint(backupFh, line) + } + + origFh, err := os.Open(path) + if err != nil { + log.Fatalf("could not open original file %s: %v", path, err) + } + + _, err = io.Copy(backupFh, origFh) + if err != nil { + log.Fatalf("could not copy: %v", err) + } + + backupFh.Close() + origFh.Close() + + err = copyFile(backupPath, path) + if err != nil { + log.Fatalf("could not rename %s -> %s: %v", backupPath, path, err) + } + +} + +func readGitConfigOrganization() string { + cfg, err := config.LoadConfig(config.GlobalScope) + if err != nil { + panic(err) + } + + for _, sec := range cfg.Raw.Sections { + if sec.Name != "user" { + continue + } + // codespell:ignore + organizations := sec.OptionAll("organization") + + for _, organization := range organizations { + if organization != "" { + return organization + } + } + } + + return "" +} + +func (s *lintSpdx) init() { + var err error + + s.extsMap = map[string]func(path string){ + ".sh": s.dontfix, + ".go": s.gofix, + ".c": s.dontfix, + ".h": s.dontfix, + ".py": s.dontfix, + ".rs": s.dontfix, + ".yaml": s.yamlfix, + ".yml": s.yamlfix, + "Dockerfile": s.dockerfilefix, + } + + s.pathsToFix = make([]string, 0) + + s.ownPath, err = os.Executable() + if err != nil { + log.Fatalf("could not determine executable path: %v", err) + } + + s.organization = readGitConfigOrganization() + +} + +func (s *lintSpdx) pathMatch(path string) (func(path string), bool) { + f, found := s.extsMap[filepath.Ext(path)] + if !found { + f, found = s.extsMap[filepath.Base(path)] + } + + return f, found +} + +func (s *lintSpdx) hasSpdx(path string) bool { + scriptPath := filepath.Join(filepath.Dir(s.ownPath), "..", "spdx-check.sh") + + cmd := exec.Command(scriptPath, path) + err := cmd.Run() + + return err == nil +} + +func (s *lintSpdx) match(path string) bool { + if s.extsMap == nil { + s.init() + } + + if strings.Contains(path, "/vendor/") { + return false + } + _, found := s.pathMatch(path) + if !found { + return false + } + if !s.hasSpdx(path) { + s.pathsToFix = append(s.pathsToFix, path) + return true + } + + return false +} + +func (s *lintSpdx) do() error { + if s.organization == "" { + log.Printf("could not read organization from git config, cannot fix copyrights") + return nil + } + + for _, path := range s.pathsToFix { + extFixFunc, found := s.pathMatch(path) + if !found { + continue + } + + extFixFunc(path) + } + + return nil +} + +func copyFile(srcPath, dstPath string) error { + src, err := os.Open(srcPath) + if err != nil { + return err + } + defer src.Close() + + dst, err := os.Create(dstPath) + if err != nil { + return err + } + defer dst.Close() + + _, err = io.Copy(dst, src) + + return err +} diff --git a/tools/git-change-exec/actions-test.go b/tools/git-change-exec/actions-test.go new file mode 100644 index 0000000000..55044dc1f6 --- /dev/null +++ b/tools/git-change-exec/actions-test.go @@ -0,0 +1,87 @@ +// Copyright (c) 2024 Zededa, Inc. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "path/filepath" + "strings" +) + +type pillarTestAction struct{} + +func commonIgnore(path string) bool { + if filepath.Ext(path) == "md" { + return true + } + + if filepath.Base(path) == "README" { + return true + } + + return false +} + +func (b pillarTestAction) match(path string) bool { + if commonIgnore(path) { + return false + } + + ignorePaths := []string{ + "pkg/pillar/agentlog/cmd/", + "pkg/pillar/docs", + } + + for _, ignorePath := range ignorePaths { + if strings.HasPrefix(path, ignorePath) { + return false + } + } + return strings.HasPrefix(path, "pkg/pillar") +} + +func (b pillarTestAction) do() error { + return execMakeTest("pkg/pillar") +} + +type getDepsTestAction struct{} + +func (g getDepsTestAction) match(path string) bool { + if commonIgnore(path) { + return false + } + + return strings.HasPrefix(path, "tools/get-deps") + +} +func (g getDepsTestAction) do() error { + return execMakeTest("tools/get-deps") +} + +type gitChangeExecTest struct{} + +func (g gitChangeExecTest) match(path string) bool { + if commonIgnore(path) { + return false + } + + return strings.HasPrefix(path, "tools/git-change-exec") + +} +func (g gitChangeExecTest) do() error { + return execMakeTest("tools/git-change-exec") +} + +type bpftraceCompilerExecTest struct{} + +func (bpftraceCompilerExecTest) match(path string) bool { + if commonIgnore(path) { + return false + } + + return strings.HasPrefix(path, "eve-tools/bpftrace-compiler") + +} +func (bpftraceCompilerExecTest) do() error { + return execMakeTest("eve-tools/bpftrace-compiler") +} diff --git a/tools/git-change-exec/actions.go b/tools/git-change-exec/actions.go new file mode 100644 index 0000000000..105fb49bc5 --- /dev/null +++ b/tools/git-change-exec/actions.go @@ -0,0 +1,50 @@ +// Copyright (c) 2024 Zededa, Inc. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "os" + "os/exec" + "reflect" +) + +type action interface { + match(path string) bool + do() error +} + +func id(i any) string { + ty := reflect.TypeOf(i) + if ty.Name() == "" { + ty = reflect.TypeOf(i).Elem() + } + return ty.Name() +} + +func execCmdWithDefaults(name string, args ...string) *exec.Cmd { + cmd := exec.Command(name, args...) + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + + return cmd +} + +func execMakeTest(path string) error { + return execCmdWithDefaults("make", "-C", path, "test").Run() +} + +// Do not forget to add your Action HERE +var actions = map[string][]action{ + "test": { + pillarTestAction{}, + gitChangeExecTest{}, + getDepsTestAction{}, + bpftraceCompilerExecTest{}, + }, + "lint": { + &lintSpdx{}, + }, +} diff --git a/tools/git-change-exec/actions_test.go b/tools/git-change-exec/actions_test.go new file mode 100644 index 0000000000..d5eed29b36 --- /dev/null +++ b/tools/git-change-exec/actions_test.go @@ -0,0 +1,15 @@ +// Copyright (c) 2024 Zededa, Inc. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import "testing" + +func TestId(t *testing.T) { + p := pillarTestAction{} + + id := id(p) + if id != "pillarTestAction" { + t.Fatalf("wrong id: %s\n", id) + } +} diff --git a/tools/git-change-exec/go.mod b/tools/git-change-exec/go.mod new file mode 100644 index 0000000000..4a46f56caa --- /dev/null +++ b/tools/git-change-exec/go.mod @@ -0,0 +1,34 @@ +module git-change-exec + +go 1.22 + +require ( + github.com/go-git/go-git/v5 v5.12.0 + github.com/spf13/cobra v1.8.1 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/tools v0.13.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect +) diff --git a/tools/git-change-exec/go.sum b/tools/git-change-exec/go.sum new file mode 100644 index 0000000000..83b63c779f --- /dev/null +++ b/tools/git-change-exec/go.sum @@ -0,0 +1,153 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tools/git-change-exec/main.go b/tools/git-change-exec/main.go new file mode 100644 index 0000000000..d3143820b3 --- /dev/null +++ b/tools/git-change-exec/main.go @@ -0,0 +1,307 @@ +// Copyright (c) 2024 Zededa, Inc. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "log" + "os" + "path/filepath" + "strings" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/storer" + "github.com/spf13/cobra" +) + +var dryRun = true +var debug = false + +type gitChangeExec struct { + actionsToCheck []action + actionDos map[action]struct{} + gitPath string + g *git.Repository + visitedPaths map[string]struct{} +} + +func debugLog(fmt string, args ...any) { + if debug { + log.Printf(fmt, args...) + } +} + +func newGitChangeExec() gitChangeExec { + return gitChangeExec{ + actionDos: map[action]struct{}{}, + actionsToCheck: []action{}, + visitedPaths: map[string]struct{}{}, + } +} + +func main() { + + actionCategories := []string{} + + for c := range actions { + actionCategories = append(actionCategories, c) + } + + rootCmd := cobra.Command{ + Args: cobra.MinimumNArgs(1), + Use: strings.Join(actionCategories, "|"), + Run: func(_ *cobra.Command, args []string) { + gce := newGitChangeExec() + + for _, category := range args { + if len(actions[category]) == 0 { + log.Fatalf("could not find actions for %s", category) + } + + gce.actionsToCheck = append(gce.actionsToCheck, actions[category]...) + } + + currentPath, err := os.Getwd() + if err != nil { + log.Fatalf("getting current working directory: %v", err) + } + + defer func() { + err := os.Chdir(currentPath) + if err != nil { + log.Printf("could not change back to previous dir %s: %v", currentPath, err) + } + }() + + gce.g, err = git.PlainOpenWithOptions("./", &git.PlainOpenOptions{DetectDotGit: true}) + if err != nil { + log.Fatalf("open git path %s failed: %v", gce.gitPath, err) + } + + wt, err := gce.g.Worktree() + if err != nil { + log.Fatalf("could not determine worktree: %v", err) + } + gce.gitPath = wt.Filesystem.Root() + + err = os.Chdir(gce.gitPath) + if err != nil { + log.Fatalf("could not change to %s: %v", gce.gitPath, err) + } + + gce.fetchOrigin() + + gce.collectActionsGitTree() + gce.collectDirtyGitTree() + + gce.runActionDos() + }, + } + + rootCmd.PersistentFlags().BoolVarP(&dryRun, "dry-run", "d", false, "") + err := rootCmd.Execute() + if err != nil { + log.Fatalf("corba failed with: %v", err) + } + +} + +func (gce *gitChangeExec) fetchOrigin() { + err := gce.g.Fetch(&git.FetchOptions{ + RemoteName: "origin", + Tags: git.AllTags, + }) + if err != nil { + debugLog("fetching from origin failed: %v", err) + } +} + +func (gce *gitChangeExec) collectActionsGitTree() { + + logIter, err := gce.g.Log(&git.LogOptions{}) + if err != nil { + log.Fatalf("getting log failed: %v", err) + } + + branchHead, err := logIter.Next() + if err != nil { + log.Fatalf("getting log.Next failed: %v", err) + } + + commonBase := gce.findCommonBase(branchHead) + + logIter, err = gce.g.Log(&git.LogOptions{}) + if err != nil { + log.Fatalf("getting log for iteration failed: %v", err) + } + + err = logIter.ForEach(func(c *object.Commit) error { + for _, cb := range commonBase { + if c.Hash == cb.Hash { + return storer.ErrStop + } + } + + commitStats, err := c.Stats() + if err != nil { + log.Fatalf("getting commit stats failed: %v", err) + } + + for _, st := range commitStats { + gce.addActionByPath(st.Name) + } + + return nil + }) + logIter.Close() + + if err != nil { + log.Fatalf("iterating over commits failed: %v", err) + } +} + +func (gce *gitChangeExec) findCommonBase(branchHead *object.Commit) []*object.Commit { + var commonBase []*object.Commit + masterRef := gce.retrieveMasterRef() + + refs := masterRef + refs = append(refs, gce.retrieveLtsRefs()...) + + for _, ref := range refs { + commit, err := gce.g.CommitObject(ref.Hash()) + if err != nil { + log.Printf("retrieve commit object from ref %v failed: %v", ref, err) + continue + } + commonBase = append(commonBase, commit) + addBase, err := branchHead.MergeBase(commit) + if err != nil { + debugLog("finding merge base failed: %v", err) + } + commonBase = append(commonBase, addBase...) + } + return commonBase +} + +func (gce *gitChangeExec) retrieveLtsRefs() []*plumbing.Reference { + ret := []*plumbing.Reference{} + + refs, err := gce.g.References() + if err != nil { + log.Printf("retrieve refs failed: %v", err) + } + + err = refs.ForEach(func(r *plumbing.Reference) error { + // f.e. refs/remotes/origin/10.4-stable + if strings.HasPrefix(r.Name().String(), "refs/remotes/origin") && + strings.HasSuffix(r.Name().String(), "-stable") { + ret = append(ret, r) + } + return nil + }) + + if err != nil { + log.Printf("iterating over refs failed: %v", err) + } + + return ret +} + +func (gce *gitChangeExec) retrieveMasterRef() []*plumbing.Reference { + masterRefs := []*plumbing.Reference{} + + for _, nameOfMaster := range []string{ + "refs/heads/master", + "refs/remotes/origin/master", + } { + var err error + + masterRef, err := gce.g.Reference(plumbing.ReferenceName(nameOfMaster), true) + if err == nil { + masterRefs = append(masterRefs, masterRef) + } + } + return masterRefs +} + +func (gce *gitChangeExec) addActionByPath(path string) { + if path == "" { + return + } + fp := filepath.Join(gce.gitPath, path) + _, visited := gce.visitedPaths[fp] + if visited { + return + } + gce.visitedPaths[fp] = struct{}{} + for _, a := range gce.actionsToCheck { + if a.match(path) { + gce.actionDos[a] = struct{}{} + } + } +} + +func (gce *gitChangeExec) runActionDos() { + failed := false + for _, a := range gce.actionsToCheck { + _, found := gce.actionDos[a] + if !found { + continue + } + var err error + if !dryRun { + log.Printf("--- running %s ...", id(a)) + err = a.do() + log.Printf("--- running %s done", id(a)) + } else { + log.Printf("would run %s, but running dry ...", id(a)) + } + if err != nil { + log.Printf("%s failed with: %v", id(a), err) + failed = true + } + } + + if failed { + os.Exit(1) + } +} + +func (gce *gitChangeExec) collectDirtyGitTree() { + ignoredStatusCodes := map[git.StatusCode]struct{}{ + git.Unmodified: {}, + git.Untracked: {}, + } + worktree, err := gce.g.Worktree() + if err != nil { + log.Fatalf("getting current worktree: %v", err) + } + + stats, err := worktree.Status() + if err != nil { + log.Fatalf("getting current worktree status: %v", err) + } + + for file, gitSt := range stats { + _, foundStaging := ignoredStatusCodes[gitSt.Staging] + _, foundWorkTree := ignoredStatusCodes[gitSt.Worktree] + + if foundStaging && foundWorkTree { + continue + } + + fp := filepath.Join(gce.gitPath, file) + st, err := os.Stat(fp) + if err != nil { + continue + } + + // git-go has some bug with links, so only consider files + if !st.Mode().IsRegular() { + continue + } + gce.addActionByPath(file) + } +}