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