github.com/microsoft/fabrikate@v1.0.0-alpha.1.0.20210115014322-dc09194d0885/internal/installable/git.go (about) 1 package installable 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "os/exec" 8 "path" 9 "sync" 10 11 "github.com/microsoft/fabrikate/internal/url" 12 ) 13 14 type Git struct { 15 URL string 16 SHA string 17 Branch string 18 } 19 20 func (g Git) Install() error { 21 // deleting if it already exists 22 componentPath, err := g.GetInstallPath() 23 if err != nil { 24 return err 25 } 26 if err := os.RemoveAll(componentPath); err != nil { 27 return err 28 } 29 // clone the repo 30 if err := clone(g.URL, componentPath); err != nil { 31 return err 32 } 33 // checkout target Branch 34 if g.Branch != "" { 35 if err := checkout(componentPath, g.Branch); err != nil { 36 return err 37 } 38 } 39 40 // checkout target SHA 41 if g.SHA != "" { 42 if err := checkout(componentPath, g.SHA); err != nil { 43 return err 44 } 45 } 46 47 return nil 48 } 49 50 func (g Git) GetInstallPath() (string, error) { 51 urlPath, err := url.ToPath(g.URL) 52 if err != nil { 53 return "", err 54 } 55 56 var version string 57 if g.SHA != "" { 58 version = g.SHA 59 } else if g.Branch != "" { 60 version = g.Branch 61 } else { 62 version = "latest" 63 } 64 65 componentPath := path.Join(installDirName, urlPath, version) 66 return componentPath, nil 67 } 68 69 func (g Git) Validate() error { 70 if g.URL == "" { 71 return fmt.Errorf(`URL must be non-zero length`) 72 } 73 if g.SHA != "" && g.Branch != "" { 74 return fmt.Errorf(`Only one of SHA or Branch can be provided, "%v" and "%v" provided respectively`, g.SHA, g.Branch) 75 } 76 77 return nil 78 } 79 80 //------------------------------------------------------------------------------ 81 // Git Helpers 82 //------------------------------------------------------------------------------ 83 84 type coordinator struct { 85 coordinator *sync.Mutex // lock to ensure only one write has access to locks at a time 86 nodes map[string]*sync.RWMutex // each lock determines if the key has been successfully cloned 87 } 88 89 var pathCoordinator = coordinator{ 90 coordinator: &sync.Mutex{}, 91 nodes: map[string]*sync.RWMutex{}, 92 } 93 94 // clone performs a `git clone <repo> <dir>` 95 func clone(repo string, dir string) error { 96 coordinator := pathCoordinator.coordinator 97 nodes := pathCoordinator.nodes 98 coordinator.Lock() // establish a read lock so we can safely read from the map of locks 99 100 // If one exists, we just need to wait for it to become free; establish a lock 101 // and immediately release 102 if node, exists := nodes[dir]; exists { 103 node.RLock() 104 coordinator.Unlock() 105 node.RUnlock() 106 return nil 107 } 108 109 // It is possible that another channel attempted to create the same mutex and 110 // established a lock before this one. Do a final check to see if a lock exists 111 if _, exists := nodes[dir]; exists { 112 return nil 113 } 114 115 // create a mutex for the path 116 nodes[dir] = &sync.RWMutex{} // add a rwlock 117 118 node, exists := nodes[dir] 119 if !exists { 120 return fmt.Errorf(`error creating mutex lock for cloning repo "%v" to dir "%v"`, repo, dir) 121 } 122 123 // write lock the path to block others from cloning the same path 124 node.Lock() // establish a write lock so the other readers are blocked 125 defer node.Unlock() 126 coordinator.Unlock() 127 128 // clone the repo 129 cmd := exec.Command("git", "clone", repo, dir) 130 cmd.Env = append(cmd.Env, os.Environ()...) 131 cmd.Env = append(cmd.Env, "GIT_TERMINAL_PROMPT=0") 132 var stderr bytes.Buffer 133 cmd.Stderr = &stderr 134 if err := cmd.Run(); err != nil { 135 return fmt.Errorf(`error cloning git repository "%v" into "%v": %w`, repo, dir, err) 136 } 137 138 return nil 139 } 140 141 // checkout will perform a `git checkout <target>` for the git repository found 142 // at `repo` 143 func checkout(repo string, target string) error { 144 coordinator := pathCoordinator.coordinator 145 nodes := pathCoordinator.nodes 146 coordinator.Lock() 147 if _, exists := nodes[target]; !exists { 148 nodes[target] = &sync.RWMutex{} 149 } 150 node := nodes[target] 151 node.Lock() 152 defer node.Unlock() 153 coordinator.Unlock() 154 155 cmd := exec.Command("git", "checkout", target) 156 cmd.Dir = repo 157 var stderr bytes.Buffer 158 cmd.Stderr = &stderr 159 if err := cmd.Run(); err != nil { 160 return fmt.Errorf(`unable to checkout "%v" from in repository "%v": %w`, target, repo, err) 161 } 162 163 return nil 164 }