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 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}