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  }