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

     1  package core
     2  
     3  import (
     4  	"bufio"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"sort"
    10  
    11  	"github.com/atlassian/git-lob/providers"
    12  	"github.com/atlassian/git-lob/util"
    13  )
    14  
    15  // Do we have a remote state cache for this remote yet?
    16  func hasRemoteStateCache(remoteName string) bool {
    17  	dir := filepath.Join(util.GetGitDir(), "git-lob", "state", "remotes", remoteName)
    18  	return util.DirExists(dir)
    19  }
    20  
    21  // Gets the root directory of the remote state cache for a given remote
    22  func getRemoteStateCacheRoot(remoteName string) string {
    23  	ret := filepath.Join(util.GetGitDir(), "git-lob", "state", "remotes", remoteName)
    24  	err := os.MkdirAll(ret, 0755)
    25  	if err != nil {
    26  		util.LogErrorf("Unable to create remote state cache folder at %v: %v", ret, err)
    27  		panic(err)
    28  	}
    29  	return ret
    30  }
    31  
    32  // Gets the file name which will store a given commitSHA if binaries are thought to
    33  // be up to date at that commit on that remote
    34  // REMOVE
    35  func getRemoteStateCacheFileForCommit(remoteName, commitSHA string) string {
    36  
    37  	// Use a simple DB format based on commit SHA
    38  	// e.g. for SHA 37d1cd1e4bd8f4853002ef6a5c8211d89fc09be2
    39  	// cacheroot/37d/1cd/1e4/bd8/f48.txt
    40  	// Every commit that starts with 37d1cd1e4bd8f48 will be stored in that text file, sorted
    41  	dir := filepath.Join(getRemoteStateCacheRoot(remoteName),
    42  		commitSHA[:3], commitSHA[3:6], commitSHA[6:9], commitSHA[9:12])
    43  	err := os.MkdirAll(dir, 0755)
    44  	if err != nil {
    45  		util.LogErrorf("Unable to create remote state cache folder at %v: %v", dir, err)
    46  		panic(err)
    47  	}
    48  	file := filepath.Join(dir, commitSHA[12:15])
    49  	return file
    50  }
    51  
    52  // Gets the file name which will store when we last pushed binaries
    53  func getRemoteStateCacheFile(remoteName string) string {
    54  
    55  	// Use a simple DB format based on commit SHA
    56  	// e.g. for SHA 37d1cd1e4bd8f4853002ef6a5c8211d89fc09be2
    57  	// cacheroot/37d/1cd/1e4/bd8/f48.txt
    58  	// Every commit that starts with 37d1cd1e4bd8f48 will be stored in that text file, sorted
    59  	dir := getRemoteStateCacheRoot(remoteName)
    60  	err := os.MkdirAll(dir, 0755)
    61  	if err != nil {
    62  		util.LogErrorf("Unable to create remote state cache folder at %v: %v", dir, err)
    63  		panic(err)
    64  	}
    65  	file := filepath.Join(dir, "push_state")
    66  	return file
    67  }
    68  
    69  // Initialise the 'pushed' markers for all recent commits, if we can be sure we can do it
    70  // Most common case: just after clone
    71  // Returns whether we met the requirements to do this
    72  func InitSuccessfullyPushedCacheIfAppropriate() bool {
    73  	// Things get complex when you can have a combination of binaries which need fetching and
    74  	// which might need pushing. Our push cache errs on the side of caution since binaries may
    75  	// have been added from multiple sources so we check we pushed (or don't need to) before
    76  	// marking a commit as pushed.
    77  	// Fetching doesn't generally mark all commits as pushed, because you can easily have the
    78  	// case where fetch only goes back a certain distance in time, but there are still commits
    79  	// further back in history which you haven't pushed the binaries for yet.
    80  	// However, after first clone you don't want to have to check the entire history. A really
    81  	// easy shortcut is that if there are no local binaries, then there can't be anything to
    82  	// push. This is the case on first fetch after a clone, so this is where we call it for now
    83  	// We can mark all known remotes as pushed.
    84  
    85  	// Adding a new remote (e.g. a fork) will however cause everything to be checked again.
    86  	if IsLocalLOBStoreEmpty() {
    87  		// No binaries locally so everything can be marked as pushed
    88  		remotes, err := GetGitRemotes()
    89  		if err != nil {
    90  			util.LogErrorf("Unable to get remotes to mark as pushed %v\n", err.Error())
    91  			return false
    92  		}
    93  		// Mark as pushed at all refs (local branches, remote branches, tags)
    94  		refs, err := GetGitAllRefs()
    95  		if err != nil {
    96  			util.LogErrorf("Unable to get refs to mark as pushed %v\n", err.Error())
    97  			return false
    98  		}
    99  		var shas []string
   100  		for _, ref := range refs {
   101  			shas = append(shas, ref.CommitSHA)
   102  		}
   103  		shas = consolidateCommitsToLatestDescendants(shas)
   104  		for _, remote := range remotes {
   105  			err := WritePushedState(remote, shas)
   106  			if err != nil {
   107  				util.LogErrorf("Unable to write push state for %v: %v\n", remote, err.Error())
   108  				return false
   109  			}
   110  		}
   111  		return true
   112  	}
   113  	return false
   114  
   115  }
   116  
   117  func MarkAllBinariesPushed(remoteName string) error {
   118  	// Mark as pushed at all refs (local branches, remote branches, tags)
   119  	refs, err := GetGitAllRefs()
   120  	if err != nil {
   121  		return err
   122  	}
   123  	var shas []string
   124  	for _, ref := range refs {
   125  		shas = append(shas, ref.CommitSHA)
   126  	}
   127  	shas = consolidateCommitsToLatestDescendants(shas)
   128  	return WritePushedState(remoteName, shas)
   129  }
   130  
   131  // Record that binaries have been pushed to a given remote at a commit
   132  // replaceCommitSHA can be blank, but if provided will replace a previously inserted SHA
   133  // If you use replaceCommitSHA, it MUST BE an ancestor of commitSHA
   134  func MarkBinariesAsPushed(remoteName, commitSHA, replaceCommitSHA string) error {
   135  	if !GitRefIsFullSHA(commitSHA) {
   136  		return fmt.Errorf("Invalid commit SHA, must be full 40 char SHA, not '%v'", commitSHA)
   137  	}
   138  	shas := GetPushedCommits(remoteName)
   139  
   140  	// confirm not there already
   141  	alreadyPresent, _ := util.StringBinarySearch(shas, commitSHA)
   142  	if alreadyPresent {
   143  		return nil
   144  	}
   145  
   146  	// insert or append, then re-sort
   147  	if replaceCommitSHA != "" {
   148  		//util.LogDebugf("Updating remote state for %v to mark %v as pushed (replaces %v)\n", remoteName, commitSHA, replaceCommitSHA)
   149  		found, insertAt := util.StringBinarySearch(shas, replaceCommitSHA)
   150  		if found {
   151  			shas[insertAt] = commitSHA
   152  		} else {
   153  			shas = append(shas, commitSHA)
   154  		}
   155  	} else {
   156  		//util.LogDebugf("Updating remote state for %v to mark %v as pushed\n", remoteName, commitSHA)
   157  		shas = append(shas, commitSHA)
   158  	}
   159  	sort.Strings(shas)
   160  	return WritePushedState(remoteName, shas)
   161  }
   162  
   163  // Overwrite entire pushed state for a remote
   164  func WritePushedState(remoteName string, shas []string) error {
   165  
   166  	filename := getRemoteStateCacheFile(remoteName)
   167  	// we just write the whole thing, sorted
   168  	sort.Strings(shas)
   169  	f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
   170  	if err != nil {
   171  		return errors.New(fmt.Sprintf("Unable to write cache file %v: %v", filename, err.Error()))
   172  	}
   173  	defer f.Close()
   174  	for _, sha := range shas {
   175  		// Have to re-insert the line break
   176  		f.WriteString(sha + "\n")
   177  	}
   178  
   179  	return nil
   180  }
   181  
   182  // Get a list of commits that have been pushed for a remote
   183  // remoteName can be "*" to return pushed list for all remotes combined
   184  func GetPushedCommits(remoteName string) []string {
   185  	var shas []string
   186  	if remoteName == "*" {
   187  		remotes, err := GetGitRemotes()
   188  		if err != nil {
   189  			return []string{}
   190  		}
   191  		for _, remote := range remotes {
   192  			rshas := GetPushedCommits(remote)
   193  			shas = append(shas, rshas...)
   194  		}
   195  
   196  	} else {
   197  		filename := getRemoteStateCacheFile(remoteName)
   198  		f, err := os.OpenFile(filename, os.O_RDONLY, 0644)
   199  		if err != nil {
   200  			// File missing
   201  			return []string{}
   202  		}
   203  		defer f.Close()
   204  		// Read entire file into memory and binary search
   205  		// Will already be sorted
   206  		scanner := bufio.NewScanner(f)
   207  		for scanner.Scan() {
   208  			shas = append(shas, scanner.Text())
   209  		}
   210  
   211  	}
   212  	return shas
   213  }
   214  
   215  // Minimise the amount of state we retain on pushed state
   216  // As we add SHAs that are pushed we can create redundant records because some SHAs are
   217  // parents of others. This makes the subsequent retrieval of commits to push slower
   218  // So remove SHAs that are ancestors of others and just keep the later SHAs that are pushed
   219  func CleanupPushState(remoteName string) {
   220  	pushed := GetPushedCommits(remoteName)
   221  
   222  	consolidated := consolidateCommitsToLatestDescendants(pushed)
   223  
   224  	if len(consolidated) != len(pushed) {
   225  		WritePushedState(remoteName, consolidated)
   226  	}
   227  }
   228  
   229  // Take a list of commit SHAs and consolidate them into another list which excludes
   230  // any commits which are ancestors of others, and those which are no longer valid
   231  // Note that this makes up to N^2 + N git calls so call infrequently
   232  func consolidateCommitsToLatestDescendants(in []string) []string {
   233  	consolidated := make([]string, 0, len(in))
   234  	for i, a := range in {
   235  		// First check this is a valid ref still (if rebased & deleted, remove)
   236  		if !GitRefOrSHAIsValid(a) {
   237  			continue
   238  		}
   239  		// If any other pushed entry is a descendent of 'a' then no reason to store 'a'
   240  		redundant := false
   241  		for j, b := range in {
   242  			if i == j {
   243  				// Don't compare to self
   244  				continue
   245  			}
   246  			if a == b {
   247  				// Duplicate, remove earliest
   248  				if i < j {
   249  					redundant = true
   250  					break
   251  				} else {
   252  					continue
   253  				}
   254  			}
   255  			isancestor, err := GitIsAncestor(a, b)
   256  			if err != nil {
   257  				// play safe & keep
   258  				continue
   259  			}
   260  			if isancestor {
   261  				redundant = true
   262  				break
   263  			}
   264  		}
   265  		if !redundant {
   266  			consolidated = append(consolidated, a)
   267  		}
   268  	}
   269  	return consolidated
   270  
   271  }
   272  
   273  // Reset the cached information about which binaries we have cached for a given remote
   274  // Warning: this will make the next push expensive while it recalculates
   275  func ResetPushedBinaryState(remoteName string) error {
   276  	return os.RemoveAll(getRemoteStateCacheRoot(remoteName))
   277  }
   278  
   279  // Do we have any pushed binary state recorded for a remote?
   280  func HasPushedBinaryState(remoteName string) bool {
   281  	return hasRemoteStateCache(remoteName)
   282  }
   283  
   284  // Find the most recent ancestor of ref (or itself) at which we believe we've
   285  // already pushed all binaries. Returns a blank string if none have been pushed.
   286  func FindLatestAncestorWhereBinariesPushed(remoteName, ref string) (string, error) {
   287  
   288  	// Use the list of pushed SHAs plus this ref to determine the best common ancestor
   289  	pushedSHAs := GetPushedCommits(remoteName)
   290  	if len(pushedSHAs) == 0 {
   291  		return "", nil
   292  	}
   293  
   294  	var refs = make([]string, 0, len(pushedSHAs)+1)
   295  	refs = append(refs, ref)
   296  	refs = append(refs, pushedSHAs...)
   297  	best, err := GetGitBestAncestor(refs)
   298  	return best, err
   299  }
   300  
   301  // Get a list of commits which have LOB SHAs to push, given a refspec, in forward ancestry order
   302  // Only commits which have LOBs associated will be returned on the assumption that when
   303  // child commits are marked as pushed it will also mark the parents
   304  // If the refspec is itself a range, just queries that range for binary references
   305  // If the refspec is a single ref, then finds the latest ancestor we think has been pushed already
   306  // for this remote and returns the LOBs referred to in that range. If recheck is true,
   307  // ignores the record of the last commit we think we pushed and scans entire history (slow)
   308  func GetCommitLOBsToPushForRefSpec(remoteName string, refspec *GitRefSpec, recheck bool) ([]*CommitLOBRef, error) {
   309  	var ret []*CommitLOBRef
   310  	callback := func(commit *CommitLOBRef) (quit bool, err error) {
   311  		ret = append(ret, commit)
   312  		return false, nil
   313  	}
   314  	err := WalkGitCommitLOBsToPushForRefSpec(remoteName, refspec, recheck, callback)
   315  	return ret, err
   316  }
   317  
   318  // Get a list of commits which have LOB SHAs to push, given a ref, in forward ancestry order
   319  // Only commits which have LOBs associated will be returned on the assumption that when
   320  // child commits are marked as pushed it will also mark the parents
   321  // If the refspec is a single ref, then finds the latest ancestor we think has been pushed already
   322  // for this remote and returns the LOBs referred to in that range. If recheck is true,
   323  // ignores the record of the last commit we think we pushed and scans entire history (slow)
   324  func GetCommitLOBsToPushForRef(remoteName string, ref string, recheck bool) ([]*CommitLOBRef, error) {
   325  	var ret []*CommitLOBRef
   326  	callback := func(commit *CommitLOBRef) (quit bool, err error) {
   327  		ret = append(ret, commit)
   328  		return false, nil
   329  	}
   330  	err := WalkGitCommitLOBsToPush(remoteName, ref, recheck, callback)
   331  	return ret, err
   332  }
   333  
   334  // Check with a remote provider for the presence of all data required for a given LOB
   335  // Return nil if all data is there, NotFoundErr if not
   336  func CheckRemoteLOBFilesForSHA(sha string, provider providers.SyncProvider, remoteName string) error {
   337  	// Smart provider can do better
   338  	switch p := provider.(type) {
   339  	case providers.SmartSyncProvider:
   340  		return CheckRemoteLOBFilesForSHASmart(sha, p, remoteName)
   341  	case providers.SyncProvider:
   342  		return CheckRemoteLOBFilesForSHABasic(sha, p, remoteName)
   343  	}
   344  	return nil
   345  }
   346  
   347  // CheckRemoteLOBFilesForSHA on mart providers
   348  func CheckRemoteLOBFilesForSHASmart(sha string, provider providers.SmartSyncProvider, remoteName string) error {
   349  	// Smart providers can check themselves
   350  	exists, _ := provider.LOBExists(remoteName, sha)
   351  	if !exists {
   352  		return NewNotFoundError(fmt.Sprintf("Content for %v missing from %v", sha, remoteName), sha)
   353  	}
   354  	return nil
   355  }
   356  
   357  // CheckRemoteLOBFilesForSHA on non-smart providers
   358  func CheckRemoteLOBFilesForSHABasic(sha string, provider providers.SyncProvider, remoteName string) error {
   359  
   360  	// We need LOB info to know size / how many chunks it had
   361  	var info *LOBInfo
   362  	info, err := GetLOBInfo(sha)
   363  	meta := GetLOBMetaRelativePath(sha)
   364  	if err != nil {
   365  		// We have to actually download meta file in order to figure out what else is needed
   366  		// A simple helper callback you can use to do nothing
   367  		dummyCallback := func(fileInProgress string, progressType util.ProgressCallbackType, bytesDone, totalBytes int64) (abort bool) {
   368  			return false
   369  		}
   370  		dlerr := provider.Download(remoteName, []string{meta}, os.TempDir(), false, dummyCallback)
   371  		if dlerr != nil {
   372  			return dlerr
   373  		}
   374  		metafullpath := filepath.Join(os.TempDir(), meta)
   375  		var parseerr error
   376  		info, parseerr = parseLOBInfoFromFile(metafullpath)
   377  		// delete from temp afterwards
   378  		os.Remove(metafullpath)
   379  		if parseerr != nil {
   380  			return fmt.Errorf("Unable to parse metadata from file downloaded from %v for %v: %v", remoteName, sha, parseerr.Error())
   381  		}
   382  	} else {
   383  		// We had the meta locally, so just check the file is on the remote
   384  		if !provider.FileExists(remoteName, meta) {
   385  			return NewNotFoundError(fmt.Sprintf("Meta file %v missing from %v", meta, remoteName), meta)
   386  		}
   387  	}
   388  
   389  	// Now we get the list of chunks & check they are present
   390  	for i := 0; i < info.NumChunks; i++ {
   391  		expectedSize := getLOBExpectedChunkSize(info, i)
   392  		chunk := GetLOBChunkRelativePath(sha, i)
   393  		if !provider.FileExistsAndIsOfSize(remoteName, chunk, expectedSize) {
   394  			return NewNotFoundError(fmt.Sprintf("Chunk file %v missing from %v", chunk, remoteName), chunk)
   395  		}
   396  	}
   397  
   398  	// All OK
   399  	return nil
   400  
   401  }