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 }