Monorepo for Tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

spindle/git: surface git stderr and heal corrupt repo caches

Signed-off-by: dawn <dawn@tangled.org>

author
dawn
date (Jul 21, 2026, 6:52 PM +0300) commit c11f472c parent da806b6f change-id ltpnpprm
+38 -5
+38 -5
spindle/git/git.go
··· 6 6 "fmt" 7 7 "os" 8 8 "os/exec" 9 + "path/filepath" 9 10 "strings" 10 11 "sync" 11 12 ··· 65 66 66 67 const WorkflowDir = `/.tangled/workflows` 67 68 69 + func runGit(ctx context.Context, args ...string) error { 70 + var stderr bytes.Buffer 71 + cmd := exec.CommandContext(ctx, "git", args...) 72 + cmd.Stderr = &stderr 73 + if err := cmd.Run(); err != nil { 74 + return fmt.Errorf("%w: %s", err, strings.TrimSpace(stderr.String())) 75 + } 76 + return nil 77 + } 78 + 68 79 func SparseSyncGitRepo(ctx context.Context, cloneUri, path, rev string) error { 69 80 defer repoLocks.lock(path)() 70 81 ··· 88 99 rev = "HEAD" 89 100 } 90 101 if !exist { 91 - if err := exec.CommandContext(ctx, "git", "clone", "--no-checkout", "--depth=1", "--filter=tree:0", "--revision="+rev, cloneUri, path).Run(); err != nil { 102 + if err := runGit(ctx, "clone", "--no-checkout", "--depth=1", "--filter=tree:0", "--revision="+rev, cloneUri, path); err != nil { 92 103 return fmt.Errorf("git clone: %w", err) 93 104 } 94 - if err := exec.CommandContext(ctx, "git", "-C", path, "sparse-checkout", "set", "--no-cone", WorkflowDir).Run(); err != nil { 105 + if err := runGit(ctx, "-C", path, "sparse-checkout", "set", "--no-cone", WorkflowDir); err != nil { 95 106 return fmt.Errorf("git sparse-checkout set: %w", err) 96 107 } 97 108 } else { 98 - if err := exec.CommandContext(ctx, "git", "-C", path, "fetch", "--depth=1", "--filter=tree:0", "origin", rev).Run(); err != nil { 99 - return fmt.Errorf("git fetch: %w", err) 109 + if err := runGit(ctx, "-C", path, "fetch", "--depth=1", "--filter=tree:0", "origin", rev); err != nil { 110 + // remove any locks if the repo was left in a mid fetch state 111 + removeStaleLocks(path) 112 + if retryErr := runGit(ctx, "-C", path, "fetch", "--depth=1", "--filter=tree:0", "origin", rev); retryErr != nil { 113 + // if still broken, wipe and refetch 114 + if rmErr := os.RemoveAll(path); rmErr != nil { 115 + return fmt.Errorf("git fetch: %w (cleanup failed: %v)", retryErr, rmErr) 116 + } 117 + if cloneErr := runGit(ctx, "clone", "--no-checkout", "--depth=1", "--filter=tree:0", "--revision="+rev, cloneUri, path); cloneErr != nil { 118 + return fmt.Errorf("git fetch: %w (re-clone failed: %v)", retryErr, cloneErr) 119 + } 120 + if cloneErr := runGit(ctx, "-C", path, "sparse-checkout", "set", "--no-cone", WorkflowDir); cloneErr != nil { 121 + return fmt.Errorf("git sparse-checkout set: %w", cloneErr) 122 + } 123 + } 100 124 } 101 125 } 102 - if err := exec.CommandContext(ctx, "git", "-C", path, "checkout", rev).Run(); err != nil { 126 + if err := runGit(ctx, "-C", path, "checkout", rev); err != nil { 103 127 return fmt.Errorf("git checkout: %w", err) 104 128 } 105 129 return nil 130 + } 131 + 132 + func removeStaleLocks(path string) { 133 + // removes shallow.lock, index.lock, etc., all are stale locks 134 + // worst case scenario we fall through to wipe and refetch anyway 135 + locks, _ := filepath.Glob(filepath.Join(path, ".git", "*.lock")) 136 + for _, lock := range locks { 137 + os.Remove(lock) 138 + } 106 139 } 107 140 108 141 func isDir(path string) (bool, error) {