github.com/atlassian/git-lob@v0.0.0-20150806085256-2386a5ed291a/core/checkout.go (about)

     1  package core
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  
    10  	"github.com/atlassian/git-lob/util"
    11  )
    12  
    13  // Callback can report skip,transfer (on complete), error
    14  type CheckoutCallback func(t util.ProgressCallbackType, filelob *FileLOB, err error)
    15  
    16  // Populate local placeholders with real content, if available. Do entire working copy unless limited to pathspecs
    17  func Checkout(pathspecs []string, dryRun bool, callback CheckoutCallback) error {
    18  	// We're going to scan for missing git-lob content not just by checking the working copy, but
    19  	// getting the expected content from git first. This is in case the working copy has had files
    20  	// deleted for example. We still check the content of the working copy if the file IS there
    21  	// in order to not overwrite modified files.
    22  
    23  	util.LogDebug("Checking for missing binary files in working copy")
    24  
    25  	// firstly convert any pathspecs to the root of the repo, in case this is being executed in a sub-folder
    26  	reporoot, _, err := util.GetRepoRoot()
    27  	if err != nil {
    28  		return err
    29  	}
    30  	curdir, err := os.Getwd()
    31  	if err != nil {
    32  		return err
    33  	}
    34  	var rootedpathspecs []string
    35  	for _, p := range pathspecs {
    36  		var abs string
    37  		if filepath.IsAbs(p) {
    38  			abs = p
    39  		} else {
    40  			abs = filepath.Join(curdir, p)
    41  		}
    42  		reltoroot, err := filepath.Rel(reporoot, abs)
    43  		if err != nil {
    44  			return errors.New(fmt.Sprintf("Unable to make %v relative to repo root %v", p, reporoot))
    45  		}
    46  		rootedpathspecs = append(rootedpathspecs, reltoroot)
    47  	}
    48  
    49  	// Get what git thinks we should have
    50  	filelobs, err := GetGitAllFilesAndLOBsToCheckoutAtCommit("HEAD", rootedpathspecs, nil)
    51  	if err != nil {
    52  		return err
    53  	}
    54  	var modifiedfiles []string
    55  	for _, filelob := range filelobs {
    56  		// Check each file, and if it's missing or contains the placeholder text, replace it with content
    57  		// Otherwise, assume it's been locally modified and leave it alone (user can override this with git reset/checkout if they want)
    58  		absfile := filepath.Join(reporoot, filelob.Filename)
    59  		stat, err := os.Stat(absfile)
    60  		replaceContent := false
    61  		if err == nil {
    62  			// File existed, check content (smoke test on size)
    63  			if stat.Size() == int64(SHALineLen) {
    64  				// File existed and is right size for placeholder, so check contents
    65  				placeholderContent := getLOBPlaceholderContent(filelob.SHA)
    66  				filebytes, err := ioutil.ReadFile(absfile)
    67  				if err == nil && string(filebytes) == placeholderContent {
    68  					// File content is placeholder, so replace
    69  					replaceContent = true
    70  				}
    71  			}
    72  		} else {
    73  			// File did not exist
    74  			replaceContent = true
    75  		}
    76  
    77  		if replaceContent {
    78  			if !dryRun {
    79  				err = checkoutFile(absfile, filelob.SHA)
    80  				if err != nil {
    81  					if IsNotFoundError(err) {
    82  						// most common issue, log nicely
    83  						callback(util.ProgressNotFound, filelob,
    84  							NewNotFoundError(fmt.Sprintf("%v: content not available, placeholder used [%v]", filelob.Filename, filelob.SHA[:7]),
    85  								filelob.Filename))
    86  					} else {
    87  						// Still not fatal but log full detail
    88  						callback(util.ProgressError, filelob,
    89  							errors.New(fmt.Sprintf("Can't retrieve content for %v: %v", filelob.Filename, err.Error())))
    90  					}
    91  				} else {
    92  					// Success
    93  					callback(util.ProgressTransferBytes, filelob, nil)
    94  				}
    95  				// In all cases, we've changed the content of the file. It's important we note this for later
    96  				modifiedfiles = append(modifiedfiles, filelob.Filename)
    97  			} else {
    98  				// Dry run, still call back as if we did it
    99  				callback(util.ProgressTransferBytes, filelob, nil)
   100  			}
   101  
   102  		} else {
   103  			callback(util.ProgressSkip, filelob, nil)
   104  		}
   105  
   106  	}
   107  
   108  	var retErr error
   109  	if len(modifiedfiles) > 0 {
   110  		// Modifying files, even to a state that would show as unmodified in 'git diff' (because our filters
   111  		// make sure that it is so) confuses git because the cached stat() info it stores no longer agrees with the file
   112  		// So 'git status' would report the files modified even though 'git diff' wouldn't. Confusing for the user!
   113  		// Cause git to refresh its index
   114  		retErr = GitRefreshIndexForFiles(modifiedfiles)
   115  	}
   116  
   117  	if retErr == nil {
   118  		util.LogDebug("Successfully checked the working copy")
   119  	}
   120  
   121  	return retErr
   122  
   123  }
   124  
   125  // Checkout a single file to a specific path
   126  func checkoutFile(path, sha string) error {
   127  	err := os.MkdirAll(filepath.Dir(path), 0755)
   128  	if err != nil {
   129  		return errors.New(fmt.Sprintf("Can't create parent directory of %v: %v\n", path, err.Error()))
   130  	}
   131  	f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
   132  	if err != nil {
   133  		return errors.New(fmt.Sprintf("Can't open %v for writing: %v", path, err.Error()))
   134  	}
   135  	defer f.Close()
   136  	_, err = RetrieveLOB(sha, f)
   137  	if err != nil {
   138  		// We already truncated the file so we need to re-write the placeholder contents
   139  		ioutil.WriteFile(path, []byte(getLOBPlaceholderContent(sha)), 0644)
   140  		return err
   141  	}
   142  
   143  	return nil
   144  
   145  }