1package commands 2 3import ( 4 "fmt" 5 "io" 6 "io/ioutil" 7 "net/url" 8 "os" 9 "path/filepath" 10 "strings" 11 "sync" 12 13 "github.com/git-lfs/git-lfs/v3/errors" 14 "github.com/git-lfs/git-lfs/v3/git" 15 "github.com/git-lfs/git-lfs/v3/lfs" 16 "github.com/git-lfs/git-lfs/v3/tasklog" 17 "github.com/git-lfs/git-lfs/v3/tools" 18 "github.com/git-lfs/git-lfs/v3/tq" 19 "github.com/rubyist/tracerx" 20) 21 22func uploadForRefUpdates(ctx *uploadContext, updates []*git.RefUpdate, pushAll bool) error { 23 gitscanner, err := ctx.buildGitScanner() 24 if err != nil { 25 return err 26 } 27 28 defer func() { 29 gitscanner.Close() 30 ctx.ReportErrors() 31 }() 32 33 verifyLocksForUpdates(ctx.lockVerifier, updates) 34 rightSides := make([]string, 0, len(updates)) 35 for _, update := range updates { 36 right := update.Right().Sha 37 if update.LeftCommitish() != right { 38 rightSides = append(rightSides, right) 39 } 40 } 41 for _, update := range updates { 42 // initialized here to prevent looped defer 43 q := ctx.NewQueue( 44 tq.RemoteRef(update.Right()), 45 ) 46 err := uploadLeftOrAll(gitscanner, ctx, q, rightSides, update, pushAll) 47 ctx.CollectErrors(q) 48 49 if err != nil { 50 return errors.Wrap(err, fmt.Sprintf("ref %s:", update.Left().Name)) 51 } 52 } 53 54 return nil 55} 56 57func uploadLeftOrAll(g *lfs.GitScanner, ctx *uploadContext, q *tq.TransferQueue, bases []string, update *git.RefUpdate, pushAll bool) error { 58 cb := ctx.gitScannerCallback(q) 59 if pushAll { 60 if err := g.ScanRefWithDeleted(update.LeftCommitish(), cb); err != nil { 61 return err 62 } 63 } else { 64 left := update.LeftCommitish() 65 right := update.Right().Sha 66 if left == right { 67 right = "" 68 } 69 if err := g.ScanMultiRangeToRemote(left, bases, cb); err != nil { 70 return err 71 } 72 } 73 return ctx.scannerError() 74} 75 76type uploadContext struct { 77 Remote string 78 DryRun bool 79 Manifest *tq.Manifest 80 uploadedOids tools.StringSet 81 gitfilter *lfs.GitFilter 82 83 logger *tasklog.Logger 84 meter *tq.Meter 85 86 committerName string 87 committerEmail string 88 89 lockVerifier *lockVerifier 90 91 // allowMissing specifies whether pushes containing missing/corrupt 92 // pointers should allow pushing Git blobs 93 allowMissing bool 94 95 // tracks errors from gitscanner callbacks 96 scannerErr error 97 errMu sync.Mutex 98 99 // filename => oid 100 missing map[string]string 101 corrupt map[string]string 102 otherErrs []error 103} 104 105func newUploadContext(dryRun bool) *uploadContext { 106 remote := cfg.PushRemote() 107 manifest := getTransferManifestOperationRemote("upload", remote) 108 ctx := &uploadContext{ 109 Remote: remote, 110 Manifest: manifest, 111 DryRun: dryRun, 112 uploadedOids: tools.NewStringSet(), 113 gitfilter: lfs.NewGitFilter(cfg), 114 lockVerifier: newLockVerifier(manifest), 115 allowMissing: cfg.Git.Bool("lfs.allowincompletepush", false), 116 missing: make(map[string]string), 117 corrupt: make(map[string]string), 118 otherErrs: make([]error, 0), 119 } 120 121 var sink io.Writer = os.Stdout 122 if dryRun { 123 sink = ioutil.Discard 124 } 125 126 ctx.logger = tasklog.NewLogger(sink, 127 tasklog.ForceProgress(cfg.ForceProgress()), 128 ) 129 ctx.meter = buildProgressMeter(ctx.DryRun, tq.Upload) 130 ctx.logger.Enqueue(ctx.meter) 131 ctx.committerName, ctx.committerEmail = cfg.CurrentCommitter() 132 return ctx 133} 134 135func (c *uploadContext) NewQueue(options ...tq.Option) *tq.TransferQueue { 136 return tq.NewTransferQueue(tq.Upload, c.Manifest, c.Remote, append(options, 137 tq.DryRun(c.DryRun), 138 tq.WithProgress(c.meter), 139 )...) 140} 141 142func (c *uploadContext) scannerError() error { 143 c.errMu.Lock() 144 defer c.errMu.Unlock() 145 146 return c.scannerErr 147} 148 149func (c *uploadContext) addScannerError(err error) { 150 c.errMu.Lock() 151 defer c.errMu.Unlock() 152 153 if c.scannerErr != nil { 154 c.scannerErr = fmt.Errorf("%v\n%v", c.scannerErr, err) 155 } else { 156 c.scannerErr = err 157 } 158} 159 160func (c *uploadContext) buildGitScanner() (*lfs.GitScanner, error) { 161 gitscanner := lfs.NewGitScanner(cfg, nil) 162 gitscanner.FoundLockable = func(n string) { c.lockVerifier.LockedByThem(n) } 163 gitscanner.PotentialLockables = c.lockVerifier 164 return gitscanner, gitscanner.RemoteForPush(c.Remote) 165} 166 167func (c *uploadContext) gitScannerCallback(tqueue *tq.TransferQueue) func(*lfs.WrappedPointer, error) { 168 return func(p *lfs.WrappedPointer, err error) { 169 if err != nil { 170 c.addScannerError(err) 171 } else { 172 c.UploadPointers(tqueue, p) 173 } 174 } 175} 176 177// AddUpload adds the given oid to the set of oids that have been uploaded in 178// the current process. 179func (c *uploadContext) SetUploaded(oid string) { 180 c.uploadedOids.Add(oid) 181} 182 183// HasUploaded determines if the given oid has already been uploaded in the 184// current process. 185func (c *uploadContext) HasUploaded(oid string) bool { 186 return c.uploadedOids.Contains(oid) 187} 188 189func (c *uploadContext) prepareUpload(unfiltered ...*lfs.WrappedPointer) []*lfs.WrappedPointer { 190 numUnfiltered := len(unfiltered) 191 uploadables := make([]*lfs.WrappedPointer, 0, numUnfiltered) 192 193 // XXX(taylor): temporary measure to fix duplicate (broken) results from 194 // scanner 195 uniqOids := tools.NewStringSet() 196 197 // separate out objects that _should_ be uploaded, but don't exist in 198 // .git/lfs/objects. Those will skipped if the server already has them. 199 for _, p := range unfiltered { 200 // object already uploaded in this process, or we've already 201 // seen this OID (see above), skip! 202 if uniqOids.Contains(p.Oid) || c.HasUploaded(p.Oid) || p.Size == 0 { 203 continue 204 } 205 uniqOids.Add(p.Oid) 206 207 // canUpload determines whether the current pointer "p" can be 208 // uploaded through the TransferQueue below. It is set to false 209 // only when the file is locked by someone other than the 210 // current committer. 211 var canUpload bool = true 212 213 if c.lockVerifier.LockedByThem(p.Name) { 214 // If the verification state is enabled, this failed 215 // locks verification means that the push should fail. 216 // 217 // If the state is disabled, the verification error is 218 // silent and the user can upload. 219 // 220 // If the state is undefined, the verification error is 221 // sent as a warning and the user can upload. 222 canUpload = !c.lockVerifier.Enabled() 223 } 224 225 c.lockVerifier.LockedByUs(p.Name) 226 227 if canUpload { 228 // estimate in meter early (even if it's not going into 229 // uploadables), since we will call Skip() based on the 230 // results of the download check queue. 231 c.meter.Add(p.Size) 232 233 uploadables = append(uploadables, p) 234 } 235 } 236 237 return uploadables 238} 239 240func (c *uploadContext) UploadPointers(q *tq.TransferQueue, unfiltered ...*lfs.WrappedPointer) { 241 if c.DryRun { 242 for _, p := range unfiltered { 243 if c.HasUploaded(p.Oid) { 244 continue 245 } 246 247 Print("push %s => %s", p.Oid, p.Name) 248 c.SetUploaded(p.Oid) 249 } 250 251 return 252 } 253 254 pointers := c.prepareUpload(unfiltered...) 255 for _, p := range pointers { 256 t, err := c.uploadTransfer(p) 257 if err != nil && !errors.IsCleanPointerError(err) { 258 ExitWithError(err) 259 } 260 261 q.Add(t.Name, t.Path, t.Oid, t.Size, t.Missing, nil) 262 c.SetUploaded(p.Oid) 263 } 264} 265 266func (c *uploadContext) CollectErrors(tqueue *tq.TransferQueue) { 267 tqueue.Wait() 268 269 for _, err := range tqueue.Errors() { 270 if malformed, ok := err.(*tq.MalformedObjectError); ok { 271 if malformed.Missing() { 272 c.missing[malformed.Name] = malformed.Oid 273 } else if malformed.Corrupt() { 274 c.corrupt[malformed.Name] = malformed.Oid 275 } 276 } else { 277 c.otherErrs = append(c.otherErrs, err) 278 } 279 } 280} 281 282func (c *uploadContext) ReportErrors() { 283 c.meter.Finish() 284 285 for _, err := range c.otherErrs { 286 FullError(err) 287 } 288 289 if len(c.missing) > 0 || len(c.corrupt) > 0 { 290 var action string 291 if c.allowMissing { 292 action = "missing objects" 293 } else { 294 action = "failed" 295 } 296 297 Print("LFS upload %s:", action) 298 for name, oid := range c.missing { 299 Print(" (missing) %s (%s)", name, oid) 300 } 301 for name, oid := range c.corrupt { 302 Print(" (corrupt) %s (%s)", name, oid) 303 } 304 305 if !c.allowMissing { 306 pushMissingHint := []string{ 307 "hint: Your push was rejected due to missing or corrupt local objects.", 308 "hint: You can disable this check with: 'git config lfs.allowincompletepush true'", 309 } 310 Print(strings.Join(pushMissingHint, "\n")) 311 os.Exit(2) 312 } 313 } 314 315 if len(c.otherErrs) > 0 { 316 os.Exit(2) 317 } 318 319 if c.lockVerifier.HasUnownedLocks() { 320 Print("Unable to push locked files:") 321 for _, unowned := range c.lockVerifier.UnownedLocks() { 322 Print("* %s - %s", unowned.Path(), unowned.Owners()) 323 } 324 325 if c.lockVerifier.Enabled() { 326 Exit("ERROR: Cannot update locked files.") 327 } else { 328 Error("WARNING: The above files would have halted this push.") 329 } 330 } else if c.lockVerifier.HasOwnedLocks() { 331 Print("Consider unlocking your own locked files: (`git lfs unlock <path>`)") 332 for _, owned := range c.lockVerifier.OwnedLocks() { 333 Print("* %s", owned.Path()) 334 } 335 } 336} 337 338var ( 339 githubHttps, _ = url.Parse("https://github.com") 340 githubSsh, _ = url.Parse("ssh://github.com") 341 342 // hostsWithKnownLockingSupport is a list of scheme-less hostnames 343 // (without port numbers) that are known to implement the LFS locking 344 // API. 345 // 346 // Additions are welcome. 347 hostsWithKnownLockingSupport = []*url.URL{ 348 githubHttps, githubSsh, 349 } 350) 351 352func (c *uploadContext) uploadTransfer(p *lfs.WrappedPointer) (*tq.Transfer, error) { 353 var missing bool 354 355 filename := p.Name 356 oid := p.Oid 357 358 localMediaPath, err := c.gitfilter.ObjectPath(oid) 359 if err != nil { 360 return nil, errors.Wrapf(err, "Error uploading file %s (%s)", filename, oid) 361 } 362 363 if len(filename) > 0 { 364 if missing, err = c.ensureFile(filename, localMediaPath, oid); err != nil && !errors.IsCleanPointerError(err) { 365 return nil, err 366 } 367 } 368 369 return &tq.Transfer{ 370 Name: filename, 371 Path: localMediaPath, 372 Oid: oid, 373 Size: p.Size, 374 Missing: missing, 375 }, nil 376} 377 378// ensureFile makes sure that the cleanPath exists before pushing it. If it 379// does not exist, it attempts to clean it by reading the file at smudgePath. 380func (c *uploadContext) ensureFile(smudgePath, cleanPath, oid string) (bool, error) { 381 if _, err := os.Stat(cleanPath); err == nil { 382 return false, nil 383 } 384 385 localPath := filepath.Join(cfg.LocalWorkingDir(), smudgePath) 386 file, err := os.Open(localPath) 387 if err != nil { 388 return !c.allowMissing, nil 389 } 390 391 defer file.Close() 392 393 stat, err := file.Stat() 394 if err != nil { 395 return false, err 396 } 397 398 cleaned, err := c.gitfilter.Clean(file, file.Name(), stat.Size(), nil) 399 if cleaned != nil { 400 cleaned.Teardown() 401 } 402 403 if err != nil { 404 return false, err 405 } 406 return false, nil 407} 408 409// supportsLockingAPI returns whether or not a given url is known to support 410// the LFS locking API by whether or not its hostname is included in the list 411// above. 412func supportsLockingAPI(rawurl string) bool { 413 u, err := url.Parse(rawurl) 414 if err != nil { 415 tracerx.Printf("commands: unable to parse %q to determine locking support: %v", rawurl, err) 416 return false 417 } 418 419 for _, supported := range hostsWithKnownLockingSupport { 420 if supported.Scheme == u.Scheme && 421 supported.Hostname() == u.Hostname() && 422 strings.HasPrefix(u.Path, supported.Path) { 423 return true 424 } 425 } 426 return false 427} 428 429// disableFor disables lock verification for the given lfsapi.Endpoint, 430// "endpoint". 431func disableFor(rawurl string) error { 432 tracerx.Printf("commands: disabling lock verification for %q", rawurl) 433 434 key := strings.Join([]string{"lfs", rawurl, "locksverify"}, ".") 435 436 _, err := cfg.SetGitLocalKey(key, "false") 437 return err 438} 439