This repository has no description
0

Configure Feed

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

core / spindle / git / git.go
4.2 kB 150 lines
1package git 2 3import ( 4 "bytes" 5 "context" 6 "fmt" 7 "os" 8 "os/exec" 9 "path/filepath" 10 "strings" 11 "sync" 12 13 "github.com/hashicorp/go-version" 14) 15 16// repoLocks serializes git operations per repo directory. Concurrent triggers 17// on the same repo (a push landing while a manual run is dispatched, two "Run 18// CI" clicks, etc.) resolve to the same path with different revisions; running 19// clone/fetch/checkout there in parallel collides on .git/index.lock and can 20// corrupt the dir. Locking is keyed by path so unrelated repos don't serialize. 21var repoLocks keyedMutex 22 23type keyedMutex struct { 24 mu sync.Mutex 25 m map[string]*sync.Mutex 26} 27 28// lock acquires the mutex for key and returns its unlock func. 29func (k *keyedMutex) lock(key string) func() { 30 k.mu.Lock() 31 if k.m == nil { 32 k.m = make(map[string]*sync.Mutex) 33 } 34 mu, ok := k.m[key] 35 if !ok { 36 mu = &sync.Mutex{} 37 k.m[key] = mu 38 } 39 k.mu.Unlock() 40 41 mu.Lock() 42 return mu.Unlock 43} 44 45func Version() (*version.Version, error) { 46 var buf bytes.Buffer 47 cmd := exec.Command("git", "version") 48 cmd.Stdout = &buf 49 cmd.Stderr = os.Stderr 50 err := cmd.Run() 51 if err != nil { 52 return nil, err 53 } 54 fields := strings.Fields(buf.String()) 55 if len(fields) < 3 { 56 return nil, fmt.Errorf("invalid git version: %s", buf.String()) 57 } 58 59 // version string is like: "git version 2.29.3" or "git version 2.29.3.windows.1" 60 versionString := fields[2] 61 if pos := strings.Index(versionString, "windows"); pos >= 1 { 62 versionString = versionString[:pos-1] 63 } 64 return version.NewVersion(versionString) 65} 66 67const WorkflowDir = `/.tangled/workflows` 68 69func 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 79func SparseSyncGitRepo(ctx context.Context, cloneUri, path, rev string) error { 80 defer repoLocks.lock(path)() 81 82 exist, err := isDir(path) 83 if err != nil { 84 return err 85 } 86 if exist { 87 gitDirExist, err := isDir(path + "/.git") 88 if err != nil { 89 return err 90 } 91 if !gitDirExist { 92 if err := os.RemoveAll(path); err != nil { 93 return fmt.Errorf("cleanup invalid git dir: %w", err) 94 } 95 exist = false 96 } 97 } 98 if rev == "" { 99 rev = "HEAD" 100 } 101 if !exist { 102 if err := runGit(ctx, "clone", "--no-checkout", "--depth=1", "--filter=tree:0", "--revision="+rev, cloneUri, path); err != nil { 103 return fmt.Errorf("git clone: %w", err) 104 } 105 if err := runGit(ctx, "-C", path, "sparse-checkout", "set", "--no-cone", WorkflowDir); err != nil { 106 return fmt.Errorf("git sparse-checkout set: %w", err) 107 } 108 } else { 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 } 124 } 125 } 126 if err := runGit(ctx, "-C", path, "checkout", rev); err != nil { 127 return fmt.Errorf("git checkout: %w", err) 128 } 129 return nil 130} 131 132func 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 } 139} 140 141func isDir(path string) (bool, error) { 142 info, err := os.Stat(path) 143 if err == nil && info.IsDir() { 144 return true, nil 145 } 146 if os.IsNotExist(err) { 147 return false, nil 148 } 149 return false, err 150}