Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package digger-cli for openSUSE:Factory 
checked in at 2026-10-31 23:09:57
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/digger-cli (Old)
 and      /work/SRC/openSUSE:Factory/.digger-cli.new.1255 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "digger-cli"

Sat Oct 31 23:09:57 2026 rev:58 rq:1394097 version:0.6.155

Changes:
--------
--- /work/SRC/openSUSE:Factory/digger-cli/digger-cli.changes    2026-10-08 
12:00:08.109840058 +0200
+++ /work/SRC/openSUSE:Factory/.digger-cli.new.1255/digger-cli.changes  
2026-10-31 23:13:05.164653344 +0100
@@ -1,0 +2,8 @@
+Sat Oct 31 12:30:26 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 0.6.155:
+  * perf: reuse GitHub installation tokens between clients (#2745)
+  * feat: fetch management repo policies once per job with a sparse
+    clone (#2744)
+
+-------------------------------------------------------------------

Old:
----
  digger-cli-0.6.154.obscpio

New:
----
  digger-cli-0.6.155.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ digger-cli.spec ++++++
--- /var/tmp/diff_new_pack.17LZls/_old  2026-10-31 23:13:07.326744373 +0100
+++ /var/tmp/diff_new_pack.17LZls/_new  2026-10-31 23:13:07.330744542 +0100
@@ -19,7 +19,7 @@
 %define executable_name digger
 
 Name:           digger-cli
-Version:        0.6.154
+Version:        0.6.155
 Release:        0
 Summary:        CLI for the digger open source IaC orchestration tool
 License:        Apache-2.0
@@ -28,8 +28,8 @@
 Source1:        vendor.tar.gz
 BuildRequires:  bash-completion
 BuildRequires:  fish
-BuildRequires:  go >= 1.22
 BuildRequires:  zsh
+BuildRequires:  golang(API) >= 1.25
 
 %description
 CI/CD for Terraform is tricky. To make life easier, specialised CI systems aka

++++++ _service ++++++
--- /var/tmp/diff_new_pack.17LZls/_old  2026-10-31 23:13:07.446749426 +0100
+++ /var/tmp/diff_new_pack.17LZls/_new  2026-10-31 23:13:07.451749636 +0100
@@ -6,8 +6,8 @@
     <param name="exclude">go.mod</param>
     <param name="exclude">go.work</param>
     <param name="exclude">go.work.sum</param>
-    <param name="revision">refs/tags/v0.6.154</param>
-    <param name="match-tag">v0.6.154</param>
+    <param name="revision">refs/tags/v0.6.155</param>
+    <param name="match-tag">v0.6.155</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.17LZls/_old  2026-10-31 23:13:07.511752162 +0100
+++ /var/tmp/diff_new_pack.17LZls/_new  2026-10-31 23:13:07.526752794 +0100
@@ -5,6 +5,6 @@
                 <param 
name="url">https://github.com/johanneskastl/digger</param>
               <param 
name="changesrevision">8fe377068e53e2050ff4c745388d8428d2b13bb0</param></service><service
 name="tar_scm">
                 <param 
name="url">https://github.com/diggerhq/digger.git</param>
-              <param 
name="changesrevision">853f6abf3f7ad9d2fa20b8956e5a758441ddca1e</param></service></servicedata>
+              <param 
name="changesrevision">7c237c093d7252db8c9a5dc93793f964d883a948</param></service></servicedata>
 (No newline at EOF)
 

++++++ digger-cli-0.6.154.obscpio -> digger-cli-0.6.155.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/digger-cli-0.6.154/backend/utils/github.go 
new/digger-cli-0.6.155/backend/utils/github.go
--- old/digger-cli-0.6.154/backend/utils/github.go      2026-10-07 
15:26:17.000000000 +0200
+++ new/digger-cli-0.6.155/backend/utils/github.go      2026-10-22 
15:47:39.000000000 +0200
@@ -2,6 +2,7 @@
 
 import (
        "context"
+       "crypto/sha256"
        "encoding/base64"
        "fmt"
        "log/slog"
@@ -9,6 +10,7 @@
        "os"
        "strconv"
        "strings"
+       "sync"
        "time"
 
        "github.com/bradleyfalzon/ghinstallation/v2"
@@ -23,6 +25,40 @@
 type DiggerGithubRealClientProvider struct {
 }
 
+type installationTransportKey struct {
+       appID, installationID int64
+       privateKeyHash        [32]byte
+}
+
+var installationTransports sync.Map
+
+type cachedInstallationTransport struct {
+       *ghinstallation.Transport
+       key installationTransportKey
+}
+
+func (t *cachedInstallationTransport) RoundTrip(request *net.Request) 
(*net.Response, error) {
+       response, err := t.Transport.RoundTrip(request)
+       if response != nil && response.StatusCode == net.StatusUnauthorized {
+               installationTransports.CompareAndDelete(t.key, t)
+       }
+       return response, err
+}
+
+func getInstallationTransport(appID, installationID int64, privateKey []byte) 
(*cachedInstallationTransport, error) {
+       key := installationTransportKey{appID, installationID, 
sha256.Sum256(privateKey)}
+       if transport, ok := installationTransports.Load(key); ok {
+               return transport.(*cachedInstallationTransport), nil
+       }
+       transport, err := ghinstallation.New(net.DefaultTransport, appID, 
installationID, privateKey)
+       if err != nil {
+               return nil, err
+       }
+       // Sharing the transport preserves ghinstallation's synchronized token 
refresh.
+       cached, _ := installationTransports.LoadOrStore(key, 
&cachedInstallationTransport{transport, key})
+       return cached.(*cachedInstallationTransport), nil
+}
+
 type DiggerGithubClientMockProvider struct {
        MockedHTTPClient *net.Client
 }
@@ -63,8 +99,7 @@
                }
        }
 
-       tr := net.DefaultTransport
-       itr, err := ghinstallation.New(tr, githubAppId, installationId, 
[]byte(githubAppPrivateKey))
+       itr, err := getInstallationTransport(githubAppId, installationId, 
[]byte(githubAppPrivateKey))
        if err != nil {
                slog.Error("Failed to initialize GitHub app installation",
                        "githubAppId", githubAppId,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/digger-cli-0.6.154/backend/utils/github_transport_test.go 
new/digger-cli-0.6.155/backend/utils/github_transport_test.go
--- old/digger-cli-0.6.154/backend/utils/github_transport_test.go       
1970-01-01 01:00:00.000000000 +0100
+++ new/digger-cli-0.6.155/backend/utils/github_transport_test.go       
2026-10-22 15:47:39.000000000 +0200
@@ -0,0 +1,154 @@
+package utils
+
+import (
+       "context"
+       "crypto/rand"
+       "crypto/rsa"
+       "crypto/x509"
+       "encoding/base64"
+       "encoding/json"
+       "encoding/pem"
+       "net/http"
+       "net/http/httptest"
+       "sync"
+       "sync/atomic"
+       "testing"
+       "time"
+)
+
+func installationTestKey(t *testing.T) []byte {
+       t.Helper()
+       key, err := rsa.GenerateKey(rand.Reader, 2048)
+       if err != nil {
+               t.Fatal(err)
+       }
+       return pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: 
x509.MarshalPKCS1PrivateKey(key)})
+}
+
+func testInstallationTransport(t *testing.T, app, installation int64, key 
[]byte, url string) *cachedInstallationTransport {
+       t.Helper()
+       transport, err := getInstallationTransport(app, installation, key)
+       if err != nil {
+               t.Fatal(err)
+       }
+       transport.BaseURL = url
+       t.Cleanup(func() { 
installationTransports.CompareAndDelete(transport.key, transport) })
+       return transport
+}
+
+func TestInstallationTokenReuseConcurrent(t *testing.T) {
+       var requests atomic.Int32
+       server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, r *http.Request) {
+               requests.Add(1)
+               if r.Method != "POST" || r.URL.Path != 
"/app/installations/42/access_tokens" {
+                       t.Errorf("unexpected request: %s %s", r.Method, 
r.URL.Path)
+               }
+               json.NewEncoder(w).Encode(map[string]interface{}{"token": 
"test-token", "expires_at": time.Now().Add(time.Hour)})
+       }))
+       defer server.Close()
+       key := installationTestKey(t)
+       t.Setenv("GITHUB_APP_PRIVATE_KEY_BASE64", 
base64.StdEncoding.EncodeToString(key))
+       testInstallationTransport(t, 1, 42, key, server.URL)
+       var workers sync.WaitGroup
+       for i := 0; i < 12; i++ {
+               workers.Add(1)
+               go func() {
+                       defer workers.Done()
+                       _, token, err := 
(DiggerGithubRealClientProvider{}).Get(1, 42)
+                       if err != nil {
+                               t.Error(err)
+                               return
+                       }
+                       if token == nil || *token != "test-token" {
+                               t.Error("unexpected installation token")
+                       }
+               }()
+       }
+       workers.Wait()
+       if got := requests.Load(); got != 1 {
+               t.Fatalf("token requests = %d, want 1", got)
+       }
+}
+
+func TestInstallationTokenIdentityIsolation(t *testing.T) {
+       var requests atomic.Int32
+       server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, r *http.Request) {
+               requests.Add(1)
+               json.NewEncoder(w).Encode(map[string]interface{}{"token": 
"test-token", "expires_at": time.Now().Add(time.Hour)})
+       }))
+       defer server.Close()
+       key, rotatedKey := installationTestKey(t), installationTestKey(t)
+       cases := []struct {
+               app, installation int64
+               key               []byte
+       }{{1, 42, key}, {2, 42, key}, {1, 43, key}, {1, 42, rotatedKey}}
+       for _, c := range cases {
+               transport := testInstallationTransport(t, c.app, 
c.installation, c.key, server.URL)
+               if _, err := transport.Token(context.Background()); err != nil {
+                       t.Fatal(err)
+               }
+       }
+       if got := requests.Load(); got != 4 {
+               t.Fatalf("token requests = %d, want 4", got)
+       }
+}
+
+func TestInstallationTokenRefreshAndRevocation(t *testing.T) {
+       var requests atomic.Int32
+       server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, r *http.Request) {
+               if r.URL.Path == "/resource" {
+                       w.WriteHeader(http.StatusUnauthorized)
+                       return
+               }
+               expiry := time.Now().Add(time.Hour)
+               if requests.Add(1) == 1 {
+                       expiry = time.Now().Add(30 * time.Second)
+               }
+               json.NewEncoder(w).Encode(map[string]interface{}{"token": 
"test-token", "expires_at": expiry})
+       }))
+       defer server.Close()
+       key := installationTestKey(t)
+       transport := testInstallationTransport(t, 1, 42, key, server.URL)
+       for i := 0; i < 2; i++ {
+               if _, err := transport.Token(context.Background()); err != nil {
+                       t.Fatal(err)
+               }
+       }
+       if got := requests.Load(); got != 2 {
+               t.Fatalf("token requests after expiry = %d, want 2", got)
+       }
+       request, err := http.NewRequest(http.MethodGet, server.URL+"/resource", 
nil)
+       if err != nil {
+               t.Fatal(err)
+       }
+       response, err := transport.RoundTrip(request)
+       if err != nil {
+               t.Fatal(err)
+       }
+       response.Body.Close()
+       if response.StatusCode != http.StatusUnauthorized {
+               t.Fatalf("status = %d, want 401", response.StatusCode)
+       }
+       replacement := testInstallationTransport(t, 1, 42, key, server.URL)
+       if replacement == transport {
+               t.Fatal("revoked transport was reused")
+       }
+       if _, err := replacement.Token(context.Background()); err != nil {
+               t.Fatal(err)
+       }
+       if got := requests.Load(); got != 3 {
+               t.Fatalf("token requests after revocation = %d, want 3", got)
+       }
+       response, err = transport.RoundTrip(request)
+       if err != nil {
+               t.Fatal(err)
+       }
+       response.Body.Close()
+       current, err := getInstallationTransport(1, 42, key)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if current != replacement {
+               t.Fatal("a late 401 from the old transport evicted its 
replacement")
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/digger-cli-0.6.154/docs/ce/reference/digger.yml.mdx 
new/digger-cli-0.6.155/docs/ce/reference/digger.yml.mdx
--- old/digger-cli-0.6.154/docs/ce/reference/digger.yml.mdx     2026-10-07 
15:26:17.000000000 +0200
+++ new/digger-cli-0.6.155/docs/ce/reference/digger.yml.mdx     2026-10-22 
15:47:39.000000000 +0200
@@ -216,7 +216,7 @@
 </ParamField>
 
 <ParamField path="git_timeout" type="integer" default="30">
-  Timeout in seconds for each git command (clone, checkout) Digger runs when 
loading repository configuration. Increase this for large repositories, for 
example `git_timeout: 120`. Digger reads this setting through the Git provider 
API before cloning, using the requested branch or commit. It checks 
`digger.yml` first, then `digger.yaml`. An omitted, zero, or negative value 
uses the 30-second default. The initial API lookup has its own 30-second 
timeout. The same value applies when a job clones the policy management 
repository (`DIGGER_MANAGEMENT_REPO`), including drift runs, so increase it if 
that repository is large.
+  Timeout in seconds for each git command (clone, checkout) Digger runs when 
loading repository configuration. Increase this for large repositories, for 
example `git_timeout: 120`. Digger reads this setting through the Git provider 
API before cloning, using the requested branch or commit. It checks 
`digger.yml` first, then `digger.yaml`. An omitted, zero, or negative value 
uses the 30-second default. The initial API lookup has its own 30-second 
timeout. The same value applies when a job fetches the policy management 
repository (`DIGGER_MANAGEMENT_REPO`). That repository is fetched once per job 
and only its `.rego` policy files are downloaded, so large repositories rarely 
need a higher value.
 </ParamField>
 
 <ParamField path="projects" type="array">
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/digger-cli-0.6.154/ee/cli/pkg/policy/fetch_test.go 
new/digger-cli-0.6.155/ee/cli/pkg/policy/fetch_test.go
--- old/digger-cli-0.6.154/ee/cli/pkg/policy/fetch_test.go      1970-01-01 
01:00:00.000000000 +0100
+++ new/digger-cli-0.6.155/ee/cli/pkg/policy/fetch_test.go      2026-10-22 
15:47:39.000000000 +0200
@@ -0,0 +1,83 @@
+package policy
+
+import (
+       "os"
+       "os/exec"
+       "path/filepath"
+       "testing"
+       "time"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+func writeFile(t *testing.T, p string, contents string) {
+       t.Helper()
+       require.NoError(t, os.MkdirAll(filepath.Dir(p), 0o755))
+       require.NoError(t, os.WriteFile(p, []byte(contents), 0o644))
+}
+
+func TestResolvePolicyFileOrder(t *testing.T) {
+       base := t.TempDir()
+       writeFile(t, filepath.Join(base, "policies", "access.rego"), "org")
+       writeFile(t, filepath.Join(base, "policies", "repo", "access.rego"), 
"repo")
+       writeFile(t, filepath.Join(base, "policies", "repo", "proj", 
"access.rego"), "project")
+       writeFile(t, filepath.Join(base, "dev", "access.rego"), "dev")
+       writeFile(t, filepath.Join(base, "dev", "vpc", "access.rego"), 
"dev/vpc")
+
+       got, err := resolvePolicyFile(base, "repo", "proj", "dev/vpc/subnets", 
"access.rego")
+       require.NoError(t, err)
+       assert.Equal(t, "dev/vpc", got, "deepest project dir prefix wins")
+
+       got, err = resolvePolicyFile(base, "repo", "proj", "prod/vpc", 
"access.rego")
+       require.NoError(t, err)
+       assert.Equal(t, "project", got, "falls back to 
policies/<repo>/<project>")
+
+       got, err = resolvePolicyFile(base, "repo", "other", "prod/vpc", 
"access.rego")
+       require.NoError(t, err)
+       assert.Equal(t, "repo", got)
+
+       got, err = resolvePolicyFile(base, "other-repo", "other", "prod/vpc", 
"access.rego")
+       require.NoError(t, err)
+       assert.Equal(t, "org", got)
+
+       got, err = resolvePolicyFile(base, "repo", "proj", "dev/vpc", 
"plan.rego")
+       require.NoError(t, err)
+       assert.Equal(t, "", got, "missing file resolves to empty, not error")
+}
+
+func TestManagementRepoFetchedOncePerProcess(t *testing.T) {
+       dir := t.TempDir()
+       run := func(args ...string) {
+               cmd := exec.Command("git", args...)
+               cmd.Dir = dir
+               cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=t", 
"GIT_AUTHOR_EMAIL=t@t", "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@t")
+               out, err := cmd.CombinedOutput()
+               require.NoError(t, err, "git %v: %s", args, out)
+       }
+       run("init", "-q", "-b", "main")
+       run("config", "uploadpack.allowFilter", "true")
+       writeFile(t, filepath.Join(dir, "policies", "access.rego"), "package 
digger\nallow = false\n")
+       writeFile(t, filepath.Join(dir, "dev", "main.tf"), "resource {}")
+       run("add", ".")
+       run("commit", "-q", "-m", "init")
+
+       p := DiggerRepoPolicyProvider{ManagementRepoUrl: "file://" + dir, 
GitTimeout: 30 * time.Second}
+
+       first, err := p.managementRepoDir()
+       require.NoError(t, err)
+       defer os.RemoveAll(first)
+       second, err := p.managementRepoDir()
+       require.NoError(t, err)
+       assert.Equal(t, first, second, "second lookup reuses the checkout")
+
+       access, err := p.GetAccessPolicy("org", "repo", "proj", "dev")
+       require.NoError(t, err)
+       assert.Equal(t, "package digger\nallow = false\n", access)
+
+       plan, err := p.GetPlanPolicy("org", "repo", "proj", "dev")
+       require.NoError(t, err)
+       assert.Equal(t, "", plan)
+
+       assert.NoFileExists(t, filepath.Join(first, "dev", "main.tf"), 
"non-policy files are not checked out")
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/digger-cli-0.6.154/ee/cli/pkg/policy/policy.go 
new/digger-cli-0.6.155/ee/cli/pkg/policy/policy.go
--- old/digger-cli-0.6.154/ee/cli/pkg/policy/policy.go  2026-10-07 
15:26:17.000000000 +0200
+++ new/digger-cli-0.6.155/ee/cli/pkg/policy/policy.go  2026-10-22 
15:47:39.000000000 +0200
@@ -1,6 +1,7 @@
 package policy
 
 import (
+       "sync"
        "time"
 
        "github.com/diggerhq/digger/libs/git_utils"
@@ -25,6 +26,39 @@
        GitTimeout        time.Duration
 }
 
+const managementRepoBranch = "main"
+
+// policyFileNames are the only files policy resolution ever reads, so the
+// management repo checkout is restricted to them.
+var policyFileNames = []string{"access.rego", "plan.rego", "apply.rego"}
+
+// A job checks policies several times per project. The management repo is
+// fetched once per process and the checkout reused for every lookup.
+var managementRepoCheckouts = struct {
+       sync.Mutex
+       dirs map[string]string
+}{dirs: map[string]string{}}
+
+func (p DiggerRepoPolicyProvider) managementRepoDir() (string, error) {
+       managementRepoCheckouts.Lock()
+       defer managementRepoCheckouts.Unlock()
+
+       key := p.ManagementRepoUrl + "@" + managementRepoBranch
+       if dir, ok := managementRepoCheckouts.dirs[key]; ok {
+               return dir, nil
+       }
+
+       start := time.Now()
+       dir, err := 
git_utils.CloneGitRepoSparseWithTimeout(p.ManagementRepoUrl, 
managementRepoBranch, p.GitToken, "", p.gitTimeout(), policyFileNames)
+       if err != nil {
+               return "", err
+       }
+       // TODO(measurement): remove before release; kept to compare fetch 
times on real jobs.
+       slog.Info("Management repo policies fetched", "repoUrl", 
p.ManagementRepoUrl, "duration", time.Since(start))
+       managementRepoCheckouts.dirs[key] = dir
+       return dir, nil
+}
+
 func getContents(filePath string) (string, error) {
        if _, err := os.Stat(filePath); err != nil {
                return "", err
@@ -65,38 +99,39 @@
 }
 
 func (p DiggerRepoPolicyProvider) getPolicyFileContents(repo string, 
projectName string, projectDir string, fileName string) (string, error) {
-       var contents string
-       err := 
git_utils.CloneGitRepoAndDoActionWithTimeout(p.ManagementRepoUrl, "main", "", 
p.GitToken, "", p.gitTimeout(), func(basePath string) error {
-               // we start with the project directory path prefixes as the 
highest priority
-               prefixes := GetPrefixesForPath(path.Join(basePath, projectDir), 
fileName)
-
-               // we also add a known location as a least priority item
-               orgAccesspath := path.Join(basePath, "policies", fileName)
-               repoAccesspath := path.Join(basePath, "policies", repo, 
fileName)
-               projectAccessPath := path.Join(basePath, "policies", repo, 
projectName, fileName)
-               prefixes = append(prefixes, projectAccessPath)
-               prefixes = append(prefixes, repoAccesspath)
-               prefixes = append(prefixes, orgAccesspath)
-
-               for _, pathPrefix := range prefixes {
-                       var err error
-                       contents, err = getContents(pathPrefix)
-                       if err == nil {
-                               return nil
-                       }
-                       if os.IsNotExist(err) {
-                               continue
-                       } else {
-                               return err
-                       }
-               }
-
-               return nil
-       })
+       basePath, err := p.managementRepoDir()
        if err != nil {
                return "", err
        }
-       return contents, nil
+       return resolvePolicyFile(basePath, repo, projectName, projectDir, 
fileName)
+}
+
+// resolvePolicyFile returns the contents of the first fileName found, starting
+// with the project directory prefixes and ending with the well-known
+// policies/ locations. Returns "" when no file exists at any location.
+func resolvePolicyFile(basePath string, repo string, projectName string, 
projectDir string, fileName string) (string, error) {
+       // we start with the project directory path prefixes as the highest 
priority
+       prefixes := GetPrefixesForPath(path.Join(basePath, projectDir), 
fileName)
+
+       // we also add a known location as a least priority item
+       orgAccesspath := path.Join(basePath, "policies", fileName)
+       repoAccesspath := path.Join(basePath, "policies", repo, fileName)
+       projectAccessPath := path.Join(basePath, "policies", repo, projectName, 
fileName)
+       prefixes = append(prefixes, projectAccessPath)
+       prefixes = append(prefixes, repoAccesspath)
+       prefixes = append(prefixes, orgAccesspath)
+
+       for _, pathPrefix := range prefixes {
+               contents, err := getContents(pathPrefix)
+               if err == nil {
+                       return contents, nil
+               }
+               if os.IsNotExist(err) {
+                       continue
+               }
+               return "", err
+       }
+       return "", nil
 }
 
 func (p DiggerRepoPolicyProvider) gitTimeout() time.Duration {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/digger-cli-0.6.154/libs/git_utils/clone_utils.go 
new/digger-cli-0.6.155/libs/git_utils/clone_utils.go
--- old/digger-cli-0.6.154/libs/git_utils/clone_utils.go        2026-10-07 
15:26:17.000000000 +0200
+++ new/digger-cli-0.6.155/libs/git_utils/clone_utils.go        2026-10-22 
15:47:39.000000000 +0200
@@ -87,6 +87,53 @@
        return nil
 }
 
+// CloneGitRepoSparseWithTimeout clones repoUrl at branch into a new temporary
+// directory and returns its path. Only blobs matching sparsePatterns
+// (gitignore-style, matched at any depth) are fetched, so a large repository
+// costs little more than its tree objects. If the sparse clone fails the
+// repository is cloned the ordinary shallow way instead. The caller owns the
+// returned directory.
+func CloneGitRepoSparseWithTimeout(repoUrl string, branch string, token 
string, tokenUsername string, timeout time.Duration, sparsePatterns []string) 
(string, error) {
+       dir, err := createTempDir()
+       if err != nil {
+               return "", err
+       }
+
+       git := NewGitShellWithTokenAuth(dir, token, tokenUsername)
+       if timeout > 0 {
+               git.timeout = timeout
+       }
+
+       err = git.CloneSparse(repoUrl, branch, sparsePatterns)
+       if err == nil {
+               return dir, nil
+       }
+
+       slog.Warn("Sparse clone failed, falling back to full shallow clone",
+               "repoUrl", repoUrl,
+               "branch", branch,
+               "error", err,
+       )
+       if err := os.RemoveAll(dir); err != nil {
+               slog.Warn("Failed to remove directory", "directory", dir, 
"error", err)
+       }
+       dir, err = createTempDir()
+       if err != nil {
+               return "", err
+       }
+       git = NewGitShellWithTokenAuth(dir, token, tokenUsername)
+       if timeout > 0 {
+               git.timeout = timeout
+       }
+       if err := git.Clone(repoUrl, branch); err != nil {
+               if rmErr := os.RemoveAll(dir); rmErr != nil {
+                       slog.Warn("Failed to remove directory", "directory", 
dir, "error", rmErr)
+               }
+               return "", err
+       }
+       return dir, nil
+}
+
 type GitAuth struct {
        Username      string
        Password      string // Can be either password or access token
@@ -213,3 +260,29 @@
        _, err = g.runCommand(args...)
        return err
 }
+
+// CloneSparse clones only the files matching patterns (gitignore-style,
+// matched at any depth). Blobs outside the patterns are never downloaded.
+func (g *GitShell) CloneSparse(repoURL, branch string, patterns []string) 
error {
+       authURL, err := g.formatAuthURL(repoURL)
+       if err != nil {
+               return err
+       }
+
+       args := []string{"clone"}
+       if branch != "" {
+               args = append(args, "-b", branch)
+       }
+       args = append(args, "--depth", "1", "--single-branch", 
"--filter=blob:none", "--no-checkout", authURL, g.workDir)
+       if _, err = g.runCommand(args...); err != nil {
+               return err
+       }
+
+       args = append([]string{"sparse-checkout", "set", "--no-cone"}, 
patterns...)
+       if _, err = g.runCommand(args...); err != nil {
+               return err
+       }
+
+       _, err = g.runCommand("checkout")
+       return err
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/digger-cli-0.6.154/libs/git_utils/sparse_clone_test.go 
new/digger-cli-0.6.155/libs/git_utils/sparse_clone_test.go
--- old/digger-cli-0.6.154/libs/git_utils/sparse_clone_test.go  1970-01-01 
01:00:00.000000000 +0100
+++ new/digger-cli-0.6.155/libs/git_utils/sparse_clone_test.go  2026-10-22 
15:47:39.000000000 +0200
@@ -0,0 +1,62 @@
+package git_utils
+
+import (
+       "os"
+       "os/exec"
+       "path/filepath"
+       "testing"
+       "time"
+
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+// newLocalRepo creates a repository on disk with a few policy files and one
+// large unrelated file, and returns a file:// URL to it.
+func newLocalRepo(t *testing.T) string {
+       t.Helper()
+       dir := t.TempDir()
+       run := func(args ...string) {
+               cmd := exec.Command("git", args...)
+               cmd.Dir = dir
+               cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=t", 
"GIT_AUTHOR_EMAIL=t@t", "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@t")
+               out, err := cmd.CombinedOutput()
+               require.NoError(t, err, "git %v: %s", args, out)
+       }
+       run("init", "-q", "-b", "main")
+       run("config", "uploadpack.allowFilter", "true")
+       require.NoError(t, os.MkdirAll(filepath.Join(dir, "dev", "vpc"), 0o755))
+       require.NoError(t, os.MkdirAll(filepath.Join(dir, "policies"), 0o755))
+       require.NoError(t, os.WriteFile(filepath.Join(dir, "dev", "vpc", 
"access.rego"), []byte("package digger\nallow = true\n"), 0o644))
+       require.NoError(t, os.WriteFile(filepath.Join(dir, "policies", 
"plan.rego"), []byte("package digger\n"), 0o644))
+       require.NoError(t, os.WriteFile(filepath.Join(dir, "dev", "vpc", 
"main.tf"), make([]byte, 1<<20), 0o644))
+       require.NoError(t, os.WriteFile(filepath.Join(dir, "README.md"), 
[]byte("hi"), 0o644))
+       run("add", ".")
+       run("commit", "-q", "-m", "init")
+       return "file://" + dir
+}
+
+func TestCloneGitRepoSparseWithTimeoutFetchesOnlyPolicyFiles(t *testing.T) {
+       url := newLocalRepo(t)
+
+       dir, err := CloneGitRepoSparseWithTimeout(url, "main", "", "", 
30*time.Second, []string{"access.rego", "plan.rego", "apply.rego"})
+       require.NoError(t, err)
+       defer os.RemoveAll(dir)
+
+       assert.FileExists(t, filepath.Join(dir, "dev", "vpc", "access.rego"))
+       assert.FileExists(t, filepath.Join(dir, "policies", "plan.rego"))
+       assert.NoFileExists(t, filepath.Join(dir, "dev", "vpc", "main.tf"))
+       assert.NoFileExists(t, filepath.Join(dir, "README.md"))
+
+       // The large blob must not have been downloaded at all, not merely left 
unchecked-out.
+       cmd := exec.Command("git", "cat-file", "-e", "HEAD:dev/vpc/main.tf")
+       cmd.Dir = dir
+       cmd.Env = append(os.Environ(), "GIT_NO_LAZY_FETCH=1")
+       assert.Error(t, cmd.Run(), "main.tf blob should be missing from the 
partial clone")
+}
+
+func TestCloneGitRepoSparseWithTimeoutFallsBackOnBadBranch(t *testing.T) {
+       url := newLocalRepo(t)
+       _, err := CloneGitRepoSparseWithTimeout(url, "does-not-exist", "", "", 
30*time.Second, []string{"access.rego"})
+       assert.Error(t, err)
+}

++++++ digger-cli.obsinfo ++++++
--- /var/tmp/diff_new_pack.17LZls/_old  2026-10-31 23:13:09.198823192 +0100
+++ /var/tmp/diff_new_pack.17LZls/_new  2026-10-31 23:13:09.202823360 +0100
@@ -1,5 +1,5 @@
 name: digger-cli
-version: 0.6.154
-mtime: 1791379577
-commit: 853f6abf3f7ad9d2fa20b8956e5a758441ddca1e
+version: 0.6.155
+mtime: 1792676859
+commit: 7c237c093d7252db8c9a5dc93793f964d883a948
 

++++++ vendor.tar.gz ++++++
/work/SRC/openSUSE:Factory/digger-cli/vendor.tar.gz 
/work/SRC/openSUSE:Factory/.digger-cli.new.1255/vendor.tar.gz differ: char 15, 
line 1

Reply via email to