This repository has no description
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 rev == "" {
76 rev = "HEAD"
77 }
78 if !exist {
79 if err := exec.Command("git", "clone", "--no-checkout", "--depth=1", "--filter=tree:0", "--revision="+rev, cloneUri, path).Run(); err != nil {
80 return fmt.Errorf("git clone: %w", err)
81 }
82 if err := exec.Command("git", "-C", path, "sparse-checkout", "set", "--no-cone", WorkflowDir).Run(); err != nil {
83 return fmt.Errorf("git sparse-checkout set: %w", err)
84 }
85 } else {
86 if err := exec.Command("git", "-C", path, "fetch", "--depth=1", "--filter=tree:0", "origin", rev).Run(); err != nil {
87 return fmt.Errorf("git pull: %w", err)
88 }
89 }
90 if err := exec.Command("git", "-C", path, "checkout", rev).Run(); err != nil {
91 return fmt.Errorf("git checkout: %w", err)
92 }
93 return nil
94}
95
96func isDir(path string) (bool, error) {
97 info, err := os.Stat(path)
98 if err == nil && info.IsDir() {
99 return true, nil
100 }
101 if os.IsNotExist(err) {
102 return false, nil
103 }
104 return false, err
105}