github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/kbfs/kbfsgit/runner.go (about) 1 // Copyright 2017 Keybase Inc. All rights reserved. 2 // Use of this source code is governed by a BSD 3 // license that can be found in the LICENSE file. 4 5 package kbfsgit 6 7 import ( 8 "bufio" 9 "context" 10 "encoding/json" 11 "fmt" 12 "io" 13 "os" 14 "path/filepath" 15 "runtime" 16 "runtime/pprof" 17 "strconv" 18 "strings" 19 "sync" 20 "time" 21 22 "github.com/keybase/client/go/kbfs/data" 23 "github.com/keybase/client/go/kbfs/idutil" 24 "github.com/keybase/client/go/kbfs/kbfsmd" 25 "github.com/keybase/client/go/kbfs/libfs" 26 "github.com/keybase/client/go/kbfs/libgit" 27 "github.com/keybase/client/go/kbfs/libkbfs" 28 "github.com/keybase/client/go/kbfs/tlf" 29 "github.com/keybase/client/go/kbfs/tlfhandle" 30 "github.com/keybase/client/go/libkb" 31 "github.com/keybase/client/go/logger" 32 "github.com/keybase/client/go/protocol/keybase1" 33 "github.com/pkg/errors" 34 billy "gopkg.in/src-d/go-billy.v4" 35 "gopkg.in/src-d/go-billy.v4/osfs" 36 gogit "gopkg.in/src-d/go-git.v4" 37 gogitcfg "gopkg.in/src-d/go-git.v4/config" 38 "gopkg.in/src-d/go-git.v4/plumbing" 39 gogitobj "gopkg.in/src-d/go-git.v4/plumbing/object" 40 gogitstor "gopkg.in/src-d/go-git.v4/plumbing/storer" 41 "gopkg.in/src-d/go-git.v4/storage" 42 "gopkg.in/src-d/go-git.v4/storage/filesystem" 43 ) 44 45 const ( 46 gitCmdCapabilities = "capabilities" 47 gitCmdList = "list" 48 gitCmdFetch = "fetch" 49 gitCmdPush = "push" 50 gitCmdOption = "option" 51 52 gitOptionVerbosity = "verbosity" 53 gitOptionProgress = "progress" 54 gitOptionCloning = "cloning" 55 gitOptionPushcert = "pushcert" 56 gitOptionIfAsked = "if-asked" 57 58 gitLFSInitEvent = "init" 59 gitLFSUploadEvent = "upload" 60 gitLFSDownloadEvent = "download" 61 gitLFSCompleteEvent = "complete" 62 gitLFSTerminateEvent = "terminate" 63 gitLFSProgressEvent = "progress" 64 65 // Debug tag ID for an individual git command passed to the process. 66 ctxCommandOpID = "GITCMDID" 67 68 kbfsgitPrefix = "keybase://" 69 repoSplitter = "/" 70 kbfsRepoDir = ".kbfs_git" 71 72 publicName = "public" 73 privateName = "private" 74 teamName = "team" 75 76 // localRepoRemoteName is the name of the remote that gets added 77 // locally to the config of the KBFS bare repo, pointing to the 78 // git repo stored at the `gitDir` passed to `newRunner`. 79 // 80 // In go-git, there is no way to hook two go-git.Repository 81 // instances together to do fetches/pulls between them. One of the 82 // two repos has to be defined as a "remote" to the other one in 83 // order to use the nice Fetch and Pull commands. (There might be 84 // other more involved ways to transfer objects manually 85 // one-by-one, but that seems like it would be pretty sad.) 86 // 87 // Since there is no standard remote protocol for keybase yet 88 // (that's what we're building!), it's not supported by go-git 89 // itself. That means our only option is to treat the local 90 // on-disk repo as a "remote" with respect to the bare KBFS repo, 91 // and do everything in reverse: for example, when a user does a 92 // push, we actually fetch from the local repo and write the 93 // objects into the bare repo. 94 localRepoRemoteName = "local" 95 96 packedRefsPath = "packed-refs" 97 packedRefsTempPath = "._packed-refs" 98 99 defaultMaxLooseRefs = 50 100 defaultPruneMinLooseObjects = -1 101 defaultMaxObjectPacks = 50 102 minGCInterval = 7 * 24 * time.Hour 103 104 unlockPrintBytesStatusThreshold = time.Second / 2 105 gcPrintStatusThreshold = time.Second 106 107 maxCommitsToVisitPerRef = 20 108 ) 109 110 type ctxCommandTagKey int 111 112 const ( 113 ctxCommandIDKey ctxCommandTagKey = iota 114 ) 115 116 type runnerProcessType int 117 118 const ( 119 processGit runnerProcessType = iota 120 processLFS 121 processLFSNoProgress 122 ) 123 124 type runner struct { 125 config libkbfs.Config 126 log logger.Logger 127 h *tlfhandle.Handle 128 remote string 129 repo string 130 gitDir string 131 uniqID string 132 input io.Reader 133 output io.Writer 134 errput io.Writer 135 gcDone bool 136 processType runnerProcessType 137 138 verbosity int64 139 progress bool 140 cloning bool 141 142 logSync sync.Once 143 logSyncDone sync.Once 144 145 printStageLock sync.Mutex 146 needPrintDone bool 147 stageStartTime time.Time 148 stageMemProfName string 149 stageCPUProfPath string 150 } 151 152 // ParseRepo parses a git repo in the form of keybase://<tlf-type>/<tlf>/<repo-name> 153 func ParseRepo(repo string) (tlfType tlf.Type, tlfName string, repoName string, err error) { 154 tlfAndRepo := strings.TrimPrefix(repo, kbfsgitPrefix) 155 parts := strings.Split(tlfAndRepo, repoSplitter) 156 if len(parts) != 3 { 157 return tlf.Unknown, "", "", errors.Errorf("Repo should be in the format "+ 158 "%s<tlfType>%s<tlf>%s<repo>, but got %s", 159 kbfsgitPrefix, repoSplitter, repoSplitter, tlfAndRepo) 160 } 161 162 switch parts[0] { 163 case publicName: 164 tlfType = tlf.Public 165 case privateName: 166 tlfType = tlf.Private 167 case teamName: 168 tlfType = tlf.SingleTeam 169 default: 170 return tlf.Unknown, "", "", errors.Errorf("Unrecognized TLF type: %s", parts[0]) 171 } 172 173 return tlfType, parts[1], libgit.NormalizeRepoName(parts[2]), nil 174 } 175 176 func newRunnerWithType(ctx context.Context, config libkbfs.Config, 177 remote, repo, gitDir string, input io.Reader, output, errput io.Writer, 178 processType runnerProcessType) ( 179 *runner, error) { 180 tlfType, tlfName, repoName, err := ParseRepo(repo) 181 if err != nil { 182 return nil, err 183 } 184 185 h, err := libkbfs.GetHandleFromFolderNameAndType( 186 ctx, config.KBPKI(), config.MDOps(), config, tlfName, tlfType) 187 if err != nil { 188 return nil, err 189 } 190 191 // Use the device ID and PID to make a unique ID (for generating 192 // temp files in KBFS). 193 session, err := idutil.GetCurrentSessionIfPossible( 194 ctx, config.KBPKI(), h.Type() == tlf.Public) 195 if err != nil { 196 return nil, err 197 } 198 uniqID := fmt.Sprintf("%s-%d", session.VerifyingKey.String(), os.Getpid()) 199 200 return &runner{ 201 config: config, 202 log: config.MakeLogger(""), 203 h: h, 204 remote: remote, 205 repo: repoName, 206 gitDir: gitDir, 207 uniqID: uniqID, 208 input: input, 209 output: output, 210 errput: errput, 211 processType: processType, 212 verbosity: 1, 213 progress: true, 214 }, nil 215 } 216 217 // newRunner creates a new runner for git commands. It expects `repo` 218 // to be in the form "keybase://private/user/reponame". `remote` 219 // is the local name assigned to that URL, while `gitDir` is the 220 // filepath leading to the .git directory of the caller's local 221 // on-disk repo. 222 func newRunner(ctx context.Context, config libkbfs.Config, 223 remote, repo, gitDir string, input io.Reader, output, errput io.Writer) ( 224 *runner, error) { 225 return newRunnerWithType( 226 ctx, config, remote, repo, gitDir, input, output, errput, processGit) 227 } 228 229 // handleCapabilities: from https://git-scm.com/docs/git-remote-helpers 230 // 231 // Lists the capabilities of the helper, one per line, ending with a 232 // blank line. Each capability may be preceded with *, which marks 233 // them mandatory for git versions using the remote helper to 234 // understand. Any unknown mandatory capability is a fatal error. 235 func (r *runner) handleCapabilities() error { 236 caps := []string{ 237 gitCmdFetch, 238 gitCmdPush, 239 gitCmdOption, 240 } 241 for _, c := range caps { 242 _, err := r.output.Write([]byte(c + "\n")) 243 if err != nil { 244 return err 245 } 246 } 247 _, err := r.output.Write([]byte("\n")) 248 return err 249 } 250 251 // getElapsedStr gets an additional string to append to the errput 252 // message at the end of a phase. It includes the measured time of 253 // the phase, and if verbosity is high enough, it includes the 254 // location of a memory profile taken at the end of the phase. 255 func (r *runner) getElapsedStr( 256 ctx context.Context, startTime time.Time, profName string, 257 cpuProfFullPath string) string { 258 if r.verbosity < 2 { 259 return "" 260 } 261 elapsed := r.config.Clock().Now().Sub(startTime) 262 elapsedStr := fmt.Sprintf(" [%s]", elapsed) 263 264 if r.verbosity >= 3 { 265 profName = filepath.Join(os.TempDir(), profName) 266 f, err := os.Create(profName) 267 if err != nil { 268 r.log.CDebugf(ctx, err.Error()) 269 } else { 270 runtime.GC() 271 err := pprof.WriteHeapProfile(f) 272 if err != nil { 273 r.log.CDebugf(ctx, "Couldn't write heap profile: %+v", err) 274 } 275 f.Close() 276 } 277 elapsedStr += " [memprof " + profName + "]" 278 } 279 280 if cpuProfFullPath != "" { 281 pprof.StopCPUProfile() 282 elapsedStr += " [cpuprof " + cpuProfFullPath + "]" 283 } 284 285 return elapsedStr 286 } 287 288 func (r *runner) printDoneOrErr( 289 ctx context.Context, err error, startTime time.Time) { 290 if r.verbosity < 1 { 291 return 292 } 293 profName := "mem.init.prof" 294 elapsedStr := r.getElapsedStr(ctx, startTime, profName, "") 295 var writeErr error 296 if err != nil { 297 _, writeErr = r.errput.Write([]byte(err.Error() + elapsedStr + "\n")) 298 } else { 299 _, writeErr = r.errput.Write([]byte("done." + elapsedStr + "\n")) 300 } 301 if writeErr != nil { 302 r.log.CDebugf(ctx, "Couldn't write error: %+v", err) 303 } 304 } 305 306 func (r *runner) isManagedByApp() bool { 307 switch r.h.Type() { 308 case tlf.Public: 309 // Public TLFs are never managed by the app. 310 return false 311 case tlf.SingleTeam: 312 // Single-team TLFs are always managed by the app. 313 return true 314 case tlf.Private: 315 // Only single-user private TLFs are managed by the app. So 316 // if the canonical name contains any commas, readers, or 317 // spaces, it's not managed by the app. 318 name := string(r.h.GetCanonicalName()) 319 return !strings.ContainsAny(name, " ,"+tlf.ReaderSep) 320 default: 321 panic(fmt.Sprintf("Unexpected type: %s", r.h.Type())) 322 } 323 } 324 325 func (r *runner) makeFS(ctx context.Context) (fs *libfs.FS, err error) { 326 // Only allow lazy creates for TLFs that aren't managed by the 327 // Keybase app. 328 if r.isManagedByApp() { 329 fs, _, err = libgit.GetRepoAndID( 330 ctx, r.config, r.h, r.repo, r.uniqID) 331 } else { 332 fs, _, err = libgit.GetOrCreateRepoAndID( 333 ctx, r.config, r.h, r.repo, r.uniqID) 334 } 335 if err != nil { 336 return nil, err 337 } 338 return fs, nil 339 } 340 341 func (r *runner) initRepoIfNeeded(ctx context.Context, forCmd string) ( 342 repo *gogit.Repository, fs *libfs.FS, err error) { 343 // This function might be called multiple times per function, but 344 // the subsequent calls will use the local cache. So only print 345 // these messages once. 346 if r.verbosity >= 1 { 347 var startTime time.Time 348 r.logSync.Do(func() { 349 startTime = r.config.Clock().Now() 350 _, err := r.errput.Write([]byte("Syncing with Keybase... ")) 351 if err != nil { 352 r.log.CDebugf(ctx, "Couldn't write: %+v", err) 353 } 354 }) 355 defer func() { 356 r.logSyncDone.Do(func() { r.printDoneOrErr(ctx, err, startTime) }) 357 }() 358 } 359 360 fs, err = r.makeFS(ctx) 361 if err != nil { 362 return nil, nil, err 363 } 364 365 // We don't persist remotes to the config on disk for two 366 // reasons. 1) gogit/gcfg has a bug where it can't handle 367 // backslashes in remote URLs, and 2) we don't want to persist the 368 // remotes anyway since they'll contain local paths and wouldn't 369 // make sense to other devices, plus that could leak local info. 370 var storage storage.Storer 371 storage, err = libgit.NewGitConfigWithoutRemotesStorer(fs) 372 if err != nil { 373 return nil, nil, err 374 } 375 376 if forCmd == gitCmdFetch { 377 r.log.CDebugf(ctx, "Using on-demand storer") 378 // Wrap it in an on-demand storer, so we don't try to read all the 379 // objects of big repos into memory at once. 380 storage, err = libgit.NewOnDemandStorer(storage) 381 if err != nil { 382 return nil, nil, err 383 } 384 } 385 386 config, err := storage.Config() 387 if err != nil { 388 return nil, nil, err 389 } 390 if config.Pack.Window > 0 { 391 // Turn delta compression off, both to avoid messing up the 392 // on-demand storer, and to avoid the unnecessary computation 393 // since we're not transferring the objects over a network. 394 // TODO: this results in uncompressed local git repo after 395 // fetches, so we should either run: 396 // 397 // `git repack -a -d -f --depth=250 --window=250` as needed. 398 // (via https://stackoverflow.com/questions/7102053/git-pull-without-remotely-compressing-objects) 399 // 400 // or we should document that the user should do so. 401 r.log.CDebugf(ctx, "Disabling pack compression by using a 0 window") 402 config.Pack.Window = 0 403 err = storage.SetConfig(config) 404 if err != nil { 405 return nil, nil, err 406 } 407 } 408 409 // TODO: This needs to take a server lock when initializing a 410 // repo. 411 r.log.CDebugf(ctx, "Attempting to init or open repo %s", r.repo) 412 repo, err = gogit.Init(storage, nil) 413 if err == gogit.ErrRepositoryAlreadyExists { 414 repo, err = gogit.Open(storage, nil) 415 } 416 if err != nil { 417 return nil, nil, err 418 } 419 420 return repo, fs, nil 421 } 422 423 func percent(n int64, d int64) float64 { 424 return float64(100) * (float64(n) / float64(d)) 425 } 426 427 func humanizeBytes(n int64, d int64) string { 428 const kb = 1024 429 const kbf = float64(kb) 430 const mb = kb * 1024 431 const mbf = float64(mb) 432 const gb = mb * 1024 433 const gbf = float64(gb) 434 // Special case the counting of bytes, when there's no denominator. 435 if d == 1 { 436 switch { 437 case n < kb: 438 return fmt.Sprintf("%d bytes", n) 439 case n < mb: 440 return fmt.Sprintf("%.2f KB", float64(n)/kbf) 441 case n < gb: 442 return fmt.Sprintf("%.2f MB", float64(n)/mbf) 443 } 444 return fmt.Sprintf("%.2f GB", float64(n)/gbf) 445 } 446 447 switch { 448 case d < kb: 449 return fmt.Sprintf("%d/%d bytes", n, d) 450 case d < mb: 451 return fmt.Sprintf("%.2f/%.2f KB", float64(n)/kbf, float64(d)/kbf) 452 case d < gb: 453 return fmt.Sprintf("%.2f/%.2f MB", float64(n)/mbf, float64(d)/mbf) 454 } 455 return fmt.Sprintf("%.2f/%.2f GB", float64(n)/gbf, float64(d)/gbf) 456 } 457 458 // printStageEndIfNeeded should only be used to end stages started with 459 // printStageStart. 460 func (r *runner) printStageEndIfNeeded(ctx context.Context) { 461 r.printStageLock.Lock() 462 defer r.printStageLock.Unlock() 463 // go-git grabs the lock right after plumbing.StatusIndexOffset, but before 464 // sending the Done status update. As a result, it would look like we are 465 // flushing the journal before plumbing.StatusIndexOffset is done. So 466 // instead, print "done." only if it's not printed yet. 467 if r.needPrintDone { 468 elapsedStr := r.getElapsedStr(ctx, 469 r.stageStartTime, r.stageMemProfName, r.stageCPUProfPath) 470 _, err := r.errput.Write([]byte("done." + elapsedStr + "\n")) 471 if err != nil { 472 r.log.CDebugf(ctx, "Couldn't write: %+v", err) 473 } 474 r.needPrintDone = false 475 } 476 } 477 478 func (r *runner) printStageStart(ctx context.Context, 479 toPrint []byte, memProfName, cpuProfName string) { 480 if len(toPrint) == 0 { 481 return 482 } 483 r.printStageEndIfNeeded(ctx) 484 485 r.printStageLock.Lock() 486 defer r.printStageLock.Unlock() 487 488 r.stageStartTime = r.config.Clock().Now() 489 r.stageMemProfName = memProfName 490 491 if len(cpuProfName) > 0 && r.verbosity >= 4 { 492 cpuProfPath := filepath.Join( 493 os.TempDir(), cpuProfName) 494 f, err := os.Create(cpuProfPath) 495 if err != nil { 496 r.log.CDebugf( 497 ctx, "Couldn't create CPU profile: %s", cpuProfName) 498 cpuProfPath = "" 499 } else { 500 err := pprof.StartCPUProfile(f) 501 if err != nil { 502 r.log.CDebugf(ctx, "Couldn't start CPU profile: %+v", err) 503 } 504 } 505 r.stageCPUProfPath = cpuProfPath 506 } 507 508 _, err := r.errput.Write(toPrint) 509 if err != nil { 510 r.log.CDebugf(ctx, "Couldn't write: %+v", err) 511 } 512 r.needPrintDone = true 513 } 514 515 func (r *runner) printGitJournalStart(ctx context.Context) { 516 adj := "encrypted" 517 if r.h.Type() == tlf.Public { 518 adj = "signed" 519 } 520 if r.verbosity >= 1 { 521 r.printStageStart(ctx, 522 []byte(fmt.Sprintf("Syncing %s data to Keybase: ", adj)), 523 "mem.flush.prof", "") 524 } 525 } 526 527 func (r *runner) printGitJournalMessage( 528 ctx context.Context, lastByteCount int, totalSize, sizeLeft int64) int { 529 const bytesFmt string = "(%.2f%%) %s... " 530 eraseStr := strings.Repeat("\b", lastByteCount) 531 flushed := totalSize - sizeLeft 532 if flushed < 0 { 533 flushed = 0 534 } 535 str := fmt.Sprintf( 536 bytesFmt, percent(flushed, totalSize), 537 humanizeBytes(flushed, totalSize)) 538 if r.verbosity >= 1 && r.progress { 539 _, err := r.errput.Write([]byte(eraseStr + str)) 540 if err != nil { 541 r.log.CDebugf(ctx, "Couldn't write: %+v", err) 542 } 543 } 544 return len(str) 545 } 546 547 // caller should make sure doneCh is closed when journal is all flushed. 548 func (r *runner) printJournalStatus( 549 ctx context.Context, jManager *libkbfs.JournalManager, tlfID tlf.ID, 550 doneCh <-chan struct{}, printStart func(context.Context), 551 printProgress func(context.Context, int, int64, int64) int, 552 printEnd func(context.Context)) { 553 printEnd(ctx) 554 // Note: the "first" status here gets us the number of unflushed 555 // bytes left at the time we started printing. However, we don't 556 // have the total number of bytes being flushed to the server 557 // throughout the whole operation, which would be more 558 // informative. It would be better to have that as the 559 // denominator, but there's no easy way to get it right now. 560 firstStatus, err := jManager.JournalStatus(tlfID) 561 if err != nil { 562 r.log.CDebugf(ctx, "Error getting status: %+v", err) 563 return 564 } 565 if firstStatus.UnflushedBytes == 0 { 566 return 567 } 568 printStart(ctx) 569 lastByteCount := printProgress( 570 ctx, 0, firstStatus.UnflushedBytes, firstStatus.UnflushedBytes) 571 572 r.log.CDebugf(ctx, "Waiting for %d journal bytes to flush", 573 firstStatus.UnflushedBytes) 574 575 ticker := time.NewTicker(1 * time.Second) 576 defer ticker.Stop() 577 for { 578 select { 579 case <-ticker.C: 580 status, err := jManager.JournalStatus(tlfID) 581 if err != nil { 582 r.log.CDebugf(ctx, "Error getting status: %+v", err) 583 return 584 } 585 586 lastByteCount = printProgress( 587 ctx, lastByteCount, firstStatus.UnflushedBytes, 588 status.UnflushedBytes) 589 case <-doneCh: 590 // doneCh is closed. So assume journal flushing is done and 591 // take the shortcut. 592 _ = printProgress( 593 ctx, lastByteCount, firstStatus.UnflushedBytes, 0) 594 595 printEnd(ctx) 596 return 597 } 598 } 599 } 600 601 func (r *runner) waitForJournalWithPrinters( 602 ctx context.Context, printStart func(context.Context), 603 printProgress func(context.Context, int, int64, int64) int, 604 printEnd func(context.Context)) error { 605 // See if there are any deleted repos to clean up before we flush 606 // the journal. 607 err := libgit.CleanOldDeletedReposTimeLimited(ctx, r.config, r.h) 608 if err != nil { 609 return err 610 } 611 612 rootNode, _, err := r.config.KBFSOps().GetOrCreateRootNode( 613 ctx, r.h, data.MasterBranch) 614 if err != nil { 615 return err 616 } 617 618 err = r.config.KBFSOps().SyncAll(ctx, rootNode.GetFolderBranch()) 619 if err != nil { 620 return err 621 } 622 623 jManager, err := libkbfs.GetJournalManager(r.config) 624 if err != nil { 625 r.log.CDebugf(ctx, "No journal server: %+v", err) 626 return nil 627 } 628 629 _, err = jManager.JournalStatus(rootNode.GetFolderBranch().Tlf) 630 if err != nil { 631 r.log.CDebugf(ctx, "No journal: %+v", err) 632 return nil 633 } 634 635 printDoneCh := make(chan struct{}) 636 waitDoneCh := make(chan struct{}) 637 go func() { 638 r.printJournalStatus( 639 ctx, jManager, rootNode.GetFolderBranch().Tlf, waitDoneCh, 640 printStart, printProgress, printEnd) 641 close(printDoneCh) 642 }() 643 644 // This squashes everything written to the journal into a single 645 // revision, to make sure that no partial states of the bare repo 646 // are seen by other readers of the TLF. It also waits for any 647 // necessary conflict resolution to complete. 648 err = jManager.FinishSingleOp(ctx, rootNode.GetFolderBranch().Tlf, 649 nil, keybase1.MDPriorityGit) 650 if err != nil { 651 return err 652 } 653 close(waitDoneCh) 654 <-printDoneCh 655 656 // Make sure that everything is truly flushed. 657 status, err := jManager.JournalStatus(rootNode.GetFolderBranch().Tlf) 658 if err != nil { 659 return err 660 } 661 662 if status.RevisionStart != kbfsmd.RevisionUninitialized { 663 r.log.CDebugf(ctx, "Journal status: %+v", status) 664 return errors.New("Journal is non-empty after a wait") 665 } 666 return nil 667 } 668 669 func (r *runner) waitForJournal(ctx context.Context) error { 670 return r.waitForJournalWithPrinters( 671 ctx, r.printGitJournalStart, r.printGitJournalMessage, 672 r.printStageEndIfNeeded) 673 } 674 675 // handleList: From https://git-scm.com/docs/git-remote-helpers 676 // 677 // Lists the refs, one per line, in the format "<value> <name> [<attr> 678 // …]". The value may be a hex sha1 hash, "@<dest>" for a symref, or 679 // "?" to indicate that the helper could not get the value of the 680 // ref. A space-separated list of attributes follows the name; 681 // unrecognized attributes are ignored. The list ends with a blank 682 // line. 683 func (r *runner) handleList(ctx context.Context, args []string) (err error) { 684 forPush := false 685 if len(args) == 1 && args[0] == "for-push" { 686 r.log.CDebugf(ctx, "Excluding symbolic refs during a for-push list") 687 forPush = true 688 } else if len(args) > 0 { 689 return errors.Errorf("Bad list request: %v", args) 690 } 691 692 repo, _, err := r.initRepoIfNeeded(ctx, gitCmdList) 693 if err != nil { 694 return err 695 } 696 697 refs, err := repo.References() 698 if err != nil { 699 return err 700 } 701 702 var symRefs []string 703 hashesSeen := false 704 for { 705 ref, err := refs.Next() 706 if errors.Cause(err) == io.EOF { 707 break 708 } 709 if err != nil { 710 return err 711 } 712 713 value := "" 714 switch ref.Type() { 715 case plumbing.HashReference: 716 value = ref.Hash().String() 717 hashesSeen = true 718 case plumbing.SymbolicReference: 719 value = "@" + ref.Target().String() 720 default: 721 value = "?" 722 } 723 refStr := value + " " + ref.Name().String() + "\n" 724 if ref.Type() == plumbing.SymbolicReference { 725 // Don't list any symbolic references until we're sure 726 // there's at least one object available. Otherwise 727 // cloning an empty repo will result in an error because 728 // the HEAD symbolic ref points to a ref that doesn't 729 // exist. 730 symRefs = append(symRefs, refStr) 731 continue 732 } 733 r.log.CDebugf(ctx, "Listing ref %s", refStr) 734 _, err = r.output.Write([]byte(refStr)) 735 if err != nil { 736 return err 737 } 738 } 739 740 if hashesSeen && !forPush { 741 for _, refStr := range symRefs { 742 r.log.CDebugf(ctx, "Listing symbolic ref %s", refStr) 743 _, err = r.output.Write([]byte(refStr)) 744 if err != nil { 745 return err 746 } 747 } 748 } 749 750 err = r.waitForJournal(ctx) 751 if err != nil { 752 return err 753 } 754 r.log.CDebugf(ctx, "Done waiting for journal") 755 756 _, err = r.output.Write([]byte("\n")) 757 return err 758 } 759 760 var gogitStagesToStatus = map[plumbing.StatusStage]string{ 761 plumbing.StatusCount: "Counting and decrypting: ", 762 plumbing.StatusRead: "Reading and decrypting metadata: ", 763 plumbing.StatusFixChains: "Fixing: ", 764 plumbing.StatusSort: "Sorting... ", 765 plumbing.StatusDelta: "Calculating deltas: ", 766 // For us, a "send" actually means fetch. 767 plumbing.StatusSend: "Fetching and decrypting objects: ", 768 // For us, a "fetch" actually means writing objects to 769 // the local journal. 770 plumbing.StatusFetch: "Preparing and encrypting: ", 771 plumbing.StatusIndexHash: "Indexing hashes: ", 772 plumbing.StatusIndexCRC: "Indexing CRCs: ", 773 plumbing.StatusIndexOffset: "Indexing offsets: ", 774 } 775 776 func humanizeObjects(n int, d int) string { 777 const k = 1000 778 const m = k * 1000 779 // Special case the counting of objects, when there's no denominator. 780 if d == 1 { 781 if n < k { 782 return fmt.Sprintf("%d", n) 783 } else if n < m { 784 return fmt.Sprintf("%.2fK", float64(n)/k) 785 } 786 return fmt.Sprintf("%.2fM", float64(n)/m) 787 } 788 789 if d < k { 790 return fmt.Sprintf("%d/%d", n, d) 791 } else if d < m { 792 return fmt.Sprintf("%.2f/%.2fK", float64(n)/k, float64(d)/k) 793 } 794 return fmt.Sprintf("%.2f/%.2fM", float64(n)/m, float64(d)/m) 795 } 796 797 func (r *runner) printJournalStatusUntilFlushed( 798 ctx context.Context, doneCh <-chan struct{}) { 799 rootNode, _, err := r.config.KBFSOps().GetOrCreateRootNode( 800 ctx, r.h, data.MasterBranch) 801 if err != nil { 802 r.log.CDebugf(ctx, "GetOrCreateRootNode error: %+v", err) 803 return 804 } 805 806 err = r.config.KBFSOps().SyncAll(ctx, rootNode.GetFolderBranch()) 807 if err != nil { 808 r.log.CDebugf(ctx, "SyncAll error: %+v", err) 809 return 810 } 811 812 jManager, err := libkbfs.GetJournalManager(r.config) 813 if err != nil { 814 r.log.CDebugf(ctx, "No journal server: %+v", err) 815 } 816 817 r.printJournalStatus( 818 ctx, jManager, rootNode.GetFolderBranch().Tlf, doneCh, 819 r.printGitJournalStart, r.printGitJournalMessage, 820 r.printStageEndIfNeeded) 821 } 822 823 func (r *runner) processGogitStatus(ctx context.Context, 824 statusChan <-chan plumbing.StatusUpdate, fsEvents <-chan libfs.FSEvent) { 825 if r.h.Type() == tlf.Public { 826 gogitStagesToStatus[plumbing.StatusFetch] = "Preparing and signing: " 827 } 828 829 currStage := plumbing.StatusUnknown 830 lastByteCount := 0 831 for { 832 if statusChan == nil && fsEvents == nil { 833 // statusChan is never passed in as nil. So if it's nil, it's been 834 // closed in the select/case below because receive failed. So 835 // instead of letting select block forever, we break out of the 836 // loop here. 837 break 838 } 839 select { 840 case update, ok := <-statusChan: 841 if !ok { 842 statusChan = nil 843 continue 844 } 845 if update.Stage != currStage { 846 if currStage != plumbing.StatusUnknown { 847 r.printStageEndIfNeeded(ctx) 848 } 849 r.printStageStart(ctx, 850 []byte(gogitStagesToStatus[update.Stage]), 851 fmt.Sprintf("mem.%d.prof", update.Stage), 852 fmt.Sprintf("cpu.%d.prof", update.Stage), 853 ) 854 lastByteCount = 0 855 if stage, ok := gogitStagesToStatus[update.Stage]; ok { 856 r.log.CDebugf(ctx, "Entering stage: %s - %d total objects", 857 stage, update.ObjectsTotal) 858 } 859 } 860 eraseStr := strings.Repeat("\b", lastByteCount) 861 newStr := "" 862 863 switch update.Stage { 864 case plumbing.StatusDone: 865 r.log.CDebugf(ctx, "Status processing done") 866 return 867 case plumbing.StatusCount: 868 newStr = fmt.Sprintf( 869 "%s objects... ", humanizeObjects(update.ObjectsTotal, 1)) 870 case plumbing.StatusSort: 871 default: 872 newStr = fmt.Sprintf( 873 "(%.2f%%) %s objects... ", 874 percent(int64(update.ObjectsDone), int64(update.ObjectsTotal)), 875 humanizeObjects(update.ObjectsDone, update.ObjectsTotal)) 876 } 877 878 lastByteCount = len(newStr) 879 if r.progress { 880 _, err := r.errput.Write([]byte(eraseStr + newStr)) 881 if err != nil { 882 r.log.CDebugf(ctx, "Couldn't write: %+v", err) 883 } 884 } 885 886 currStage = update.Stage 887 case fsEvent, ok := <-fsEvents: 888 if !ok { 889 fsEvents = nil 890 continue 891 } 892 switch fsEvent.EventType { 893 case libfs.FSEventLock, libfs.FSEventUnlock: 894 r.printStageEndIfNeeded(ctx) 895 // Since we flush all blocks in Lock, subsequent calls to 896 // Lock/Unlock normally don't take much time. So we only print 897 // journal status if it's been longer than 898 // unlockPrintBytesStatusThreshold and fsEvent.Done hasn't been 899 // closed. 900 timer := time.NewTimer(unlockPrintBytesStatusThreshold) 901 select { 902 case <-timer.C: 903 r.printJournalStatusUntilFlushed(ctx, fsEvent.Done) 904 case <-fsEvent.Done: 905 timer.Stop() 906 case <-ctx.Done(): 907 timer.Stop() 908 } 909 } 910 } 911 } 912 r.log.CDebugf(ctx, "Status channel closed") 913 r.printStageEndIfNeeded(ctx) 914 } 915 916 // recursiveByteCount returns a sum of the size of all files under the 917 // directory represented by `fs`. It also returns the length of the 918 // last string it printed to `r.errput` as `toErase`, to aid in 919 // overwriting the text on the next update. 920 func (r *runner) recursiveByteCount( 921 ctx context.Context, fs billy.Filesystem, totalSoFar int64, toErase int) ( 922 bytes int64, toEraseRet int, err error) { 923 fileInfos, err := fs.ReadDir("/") 924 if err != nil { 925 return 0, 0, err 926 } 927 928 for _, fi := range fileInfos { 929 if fi.IsDir() { 930 if fi.Name() == "." { 931 continue 932 } 933 chrootFS, err := fs.Chroot(fi.Name()) 934 if err != nil { 935 return 0, 0, err 936 } 937 var chrootBytes int64 938 chrootBytes, toErase, err = r.recursiveByteCount( 939 ctx, chrootFS, totalSoFar+bytes, toErase) 940 if err != nil { 941 return 0, 0, err 942 } 943 bytes += chrootBytes 944 } else { 945 bytes += fi.Size() 946 if r.progress { 947 // This function only runs if r.verbosity >= 1. 948 eraseStr := strings.Repeat("\b", toErase) 949 newStr := fmt.Sprintf( 950 "%s... ", humanizeBytes(totalSoFar+bytes, 1)) 951 toErase = len(newStr) 952 _, err := r.errput.Write([]byte(eraseStr + newStr)) 953 if err != nil { 954 return 0, 0, err 955 } 956 } 957 } 958 } 959 return bytes, toErase, nil 960 } 961 962 // statusWriter is a simple io.Writer shim that logs to `r.errput` the 963 // number of bytes written to `output`. 964 type statusWriter struct { 965 r *runner 966 output io.Writer 967 soFar int64 968 totalBytes int64 969 nextToErase int 970 } 971 972 var _ io.Writer = (*statusWriter)(nil) 973 974 func (sw *statusWriter) Write(p []byte) (n int, err error) { 975 n, err = sw.output.Write(p) 976 if err != nil { 977 return n, err 978 } 979 980 sw.soFar += int64(len(p)) 981 eraseStr := strings.Repeat("\b", sw.nextToErase) 982 newStr := fmt.Sprintf("(%.2f%%) %s... ", 983 percent(sw.soFar, sw.totalBytes), 984 humanizeBytes(sw.soFar, sw.totalBytes)) 985 _, err = sw.r.errput.Write([]byte(eraseStr + newStr)) 986 if err != nil { 987 return n, err 988 } 989 sw.nextToErase = len(newStr) 990 return n, nil 991 } 992 993 func (r *runner) copyFile( 994 ctx context.Context, from billy.Filesystem, to billy.Filesystem, 995 name string, sw *statusWriter) (err error) { 996 f, err := from.Open(name) 997 if err != nil { 998 return err 999 } 1000 defer f.Close() 1001 toF, err := to.Create(name) 1002 if err != nil { 1003 return err 1004 } 1005 defer toF.Close() 1006 1007 var w io.Writer = toF 1008 // Wrap the destination file in a status shim if we are supposed 1009 // to report progress. 1010 if sw != nil && r.progress { 1011 sw.output = toF 1012 w = sw 1013 } 1014 _, err = io.Copy(w, f) 1015 return err 1016 } 1017 1018 func (r *runner) copyFileWithCount( 1019 ctx context.Context, from billy.Filesystem, to billy.Filesystem, 1020 name, countingText, countingProf, copyingText, copyingProf string) error { 1021 var sw *statusWriter 1022 if r.verbosity >= 1 { 1023 // Get the total number of bytes we expect to fetch, for the 1024 // progress report. 1025 startTime := r.config.Clock().Now() 1026 zeroStr := fmt.Sprintf("%s... ", humanizeBytes(0, 1)) 1027 _, err := r.errput.Write( 1028 []byte(fmt.Sprintf("%s: %s", countingText, zeroStr))) 1029 if err != nil { 1030 return err 1031 } 1032 fi, err := from.Stat(name) 1033 if err != nil { 1034 return err 1035 } 1036 eraseStr := strings.Repeat("\b", len(zeroStr)) 1037 newStr := fmt.Sprintf("%s... ", humanizeBytes(fi.Size(), 1)) 1038 _, err = r.errput.Write([]byte(eraseStr + newStr)) 1039 if err != nil { 1040 return err 1041 } 1042 1043 elapsedStr := r.getElapsedStr( 1044 ctx, startTime, fmt.Sprintf("mem.%s.prof", countingProf), "") 1045 _, err = r.errput.Write([]byte("done." + elapsedStr + "\n")) 1046 if err != nil { 1047 return err 1048 } 1049 1050 sw = &statusWriter{r, nil, 0, fi.Size(), 0} 1051 _, err = r.errput.Write([]byte(fmt.Sprintf("%s: ", copyingText))) 1052 if err != nil { 1053 return err 1054 } 1055 } 1056 1057 // Copy the file directly into the other file system. 1058 startTime := r.config.Clock().Now() 1059 err := r.copyFile(ctx, from, to, name, sw) 1060 if err != nil { 1061 return err 1062 } 1063 1064 if r.verbosity >= 1 { 1065 elapsedStr := r.getElapsedStr( 1066 ctx, startTime, fmt.Sprintf("mem.%s.prof", copyingProf), "") 1067 _, err = r.errput.Write([]byte("done." + elapsedStr + "\n")) 1068 if err != nil { 1069 return err 1070 } 1071 } 1072 return nil 1073 } 1074 1075 // recursiveCopy copies the entire subdirectory rooted at `fs` to 1076 // `localFS`. 1077 func (r *runner) recursiveCopy( 1078 ctx context.Context, from billy.Filesystem, to billy.Filesystem, 1079 sw *statusWriter) (err error) { 1080 fileInfos, err := from.ReadDir("") 1081 if err != nil { 1082 return err 1083 } 1084 1085 for _, fi := range fileInfos { 1086 if fi.IsDir() { 1087 if fi.Name() == "." { 1088 continue 1089 } 1090 err := to.MkdirAll(fi.Name(), 0775) 1091 if err != nil { 1092 return err 1093 } 1094 chrootFrom, err := from.Chroot(fi.Name()) 1095 if err != nil { 1096 return err 1097 } 1098 chrootTo, err := to.Chroot(fi.Name()) 1099 if err != nil { 1100 return err 1101 } 1102 err = r.recursiveCopy(ctx, chrootFrom, chrootTo, sw) 1103 if err != nil { 1104 return err 1105 } 1106 } else { 1107 err := r.copyFile(ctx, from, to, fi.Name(), sw) 1108 if err != nil { 1109 return err 1110 } 1111 } 1112 } 1113 return nil 1114 } 1115 1116 func (r *runner) recursiveCopyWithCounts( 1117 ctx context.Context, from billy.Filesystem, to billy.Filesystem, 1118 countingText, countingProf, copyingText, copyingProf string) error { 1119 var sw *statusWriter 1120 if r.verbosity >= 1 { 1121 // Get the total number of bytes we expect to fetch, for the 1122 // progress report. 1123 startTime := r.config.Clock().Now() 1124 _, err := r.errput.Write([]byte(fmt.Sprintf("%s: ", countingText))) 1125 if err != nil { 1126 return err 1127 } 1128 b, _, err := r.recursiveByteCount(ctx, from, 0, 0) 1129 if err != nil { 1130 return err 1131 } 1132 elapsedStr := r.getElapsedStr( 1133 ctx, startTime, fmt.Sprintf("mem.%s.prof", countingProf), "") 1134 _, err = r.errput.Write([]byte("done." + elapsedStr + "\n")) 1135 if err != nil { 1136 return err 1137 } 1138 1139 sw = &statusWriter{r, nil, 0, b, 0} 1140 _, err = r.errput.Write([]byte(fmt.Sprintf("%s: ", copyingText))) 1141 if err != nil { 1142 return err 1143 } 1144 } 1145 1146 // Copy the entire subdirectory straight into the other file 1147 // system. This saves time and memory relative to going through 1148 // go-git. 1149 startTime := r.config.Clock().Now() 1150 err := r.recursiveCopy(ctx, from, to, sw) 1151 if err != nil { 1152 return err 1153 } 1154 1155 if r.verbosity >= 1 { 1156 elapsedStr := r.getElapsedStr( 1157 ctx, startTime, fmt.Sprintf("mem.%s.prof", copyingProf), "") 1158 _, err := r.errput.Write([]byte("done." + elapsedStr + "\n")) 1159 if err != nil { 1160 return err 1161 } 1162 } 1163 return nil 1164 } 1165 1166 // checkGC should only be called from the main command-processing 1167 // goroutine. 1168 func (r *runner) checkGC(ctx context.Context) (err error) { 1169 if r.gcDone { 1170 return nil 1171 } 1172 r.gcDone = true 1173 1174 if !r.isManagedByApp() { 1175 r.log.CDebugf(ctx, "Skipping GC check") 1176 return nil 1177 } 1178 1179 r.log.CDebugf(ctx, "Checking for GC") 1180 1181 var stageSync sync.Once 1182 ctx, cancel := context.WithCancel(ctx) 1183 defer cancel() 1184 go func() { 1185 timer := time.NewTimer(gcPrintStatusThreshold) 1186 select { 1187 case <-timer.C: 1188 stageSync.Do(func() { 1189 r.printStageStart(ctx, 1190 []byte("Checking repo for inefficiencies... "), 1191 "mem.gc.prof", "cpu.gc.prof") 1192 }) 1193 case <-ctx.Done(): 1194 timer.Stop() 1195 } 1196 }() 1197 defer func() { 1198 // Prevent stage from starting after we finish the stage. 1199 stageSync.Do(func() {}) 1200 if err == nil { 1201 r.printStageEndIfNeeded(ctx) 1202 } 1203 }() 1204 1205 fs, _, err := libgit.GetRepoAndID( 1206 ctx, r.config, r.h, r.repo, r.uniqID) 1207 if _, noRepo := errors.Cause(err).(libkb.RepoDoesntExistError); noRepo { 1208 r.log.CDebugf(ctx, "No such repo: %v", err) 1209 return nil 1210 } else if err != nil { 1211 return err 1212 } 1213 1214 lastGCTime, err := libgit.LastGCTime(ctx, fs) 1215 if err != nil { 1216 return err 1217 } 1218 if r.config.Clock().Now().Sub(lastGCTime) < minGCInterval { 1219 r.log.CDebugf(ctx, "Last GC happened at %s; skipping GC check", 1220 lastGCTime) 1221 return nil 1222 } 1223 1224 storage, err := libgit.NewGitConfigWithoutRemotesStorer(fs) 1225 if err != nil { 1226 return err 1227 } 1228 1229 gco := libgit.GCOptions{ 1230 MaxLooseRefs: defaultMaxLooseRefs, 1231 PruneMinLooseObjects: defaultPruneMinLooseObjects, 1232 PruneExpireTime: time.Time{}, 1233 MaxObjectPacks: defaultMaxObjectPacks, 1234 } 1235 doPackRefs, _, doPruneLoose, doObjectRepack, _, err := libgit.NeedsGC( 1236 storage, gco) 1237 if err != nil { 1238 return err 1239 } 1240 if !doPackRefs && !doPruneLoose && !doObjectRepack { 1241 r.log.CDebugf(ctx, "No GC needed") 1242 return nil 1243 } 1244 r.log.CDebugf(ctx, "GC needed: doPackRefs=%t, doPruneLoose=%t, "+ 1245 "doObjectRepack=%t", doPackRefs, doPruneLoose, doObjectRepack) 1246 command := fmt.Sprintf("keybase git gc %s", r.repo) 1247 if r.h.Type() == tlf.SingleTeam { 1248 tid := r.h.FirstResolvedWriter() 1249 teamName, err := r.config.KBPKI().GetNormalizedUsername( 1250 ctx, tid, r.config.OfflineAvailabilityForID(r.h.TlfID())) 1251 if err != nil { 1252 return err 1253 } 1254 command += fmt.Sprintf(" --team %s", teamName) 1255 } 1256 _, err = r.errput.Write([]byte( 1257 "Tip: this repo could be sped up with some " + 1258 "garbage collection. Run this command:\n")) 1259 if err != nil { 1260 return err 1261 } 1262 _, err = r.errput.Write([]byte("\t" + command + "\n")) 1263 return err 1264 } 1265 1266 // handleClone copies all the object files of a KBFS repo directly 1267 // into the local git dir, instead of using go-git to calculate the 1268 // full set of objects that are to be transferred (which is slow and 1269 // memory inefficient). If the user requested only a single branch of 1270 // cloning, this will copy more objects that necessary, but still only 1271 // a single ref will show up for the user. TODO: Maybe we should run 1272 // `git gc` for the user on the local repo? 1273 func (r *runner) handleClone(ctx context.Context) (err error) { 1274 _, _, err = r.initRepoIfNeeded(ctx, "clone") 1275 if err != nil { 1276 return err 1277 } 1278 1279 r.log.CDebugf(ctx, "Cloning into %s", r.gitDir) 1280 1281 fs, _, err := libgit.GetOrCreateRepoAndID( 1282 ctx, r.config, r.h, r.repo, r.uniqID) 1283 if err != nil { 1284 return err 1285 } 1286 fsObjects, err := fs.Chroot("objects") 1287 if err != nil { 1288 return err 1289 } 1290 1291 localObjectsPath := filepath.Join(r.gitDir, "objects") 1292 err = os.MkdirAll(localObjectsPath, 0775) 1293 if err != nil { 1294 return err 1295 } 1296 localFSObjects := osfs.New(localObjectsPath) 1297 1298 err = r.recursiveCopyWithCounts( 1299 ctx, fsObjects, localFSObjects, 1300 "Counting", "count", "Cryptographic cloning", "clone") 1301 if err != nil { 1302 return err 1303 } 1304 1305 err = r.checkGC(ctx) 1306 if err != nil { 1307 return err 1308 } 1309 1310 _, err = r.output.Write([]byte("\n")) 1311 return err 1312 } 1313 1314 // handleFetchBatch: From https://git-scm.com/docs/git-remote-helpers 1315 // 1316 // fetch <sha1> <name> 1317 // Fetches the given object, writing the necessary objects to the 1318 // database. Fetch commands are sent in a batch, one per line, 1319 // terminated with a blank line. Outputs a single blank line when all 1320 // fetch commands in the same batch are complete. Only objects which 1321 // were reported in the output of list with a sha1 may be fetched this 1322 // way. 1323 // 1324 // Optionally may output a lock <file> line indicating a file under 1325 // GIT_DIR/objects/pack which is keeping a pack until refs can be 1326 // suitably updated. 1327 func (r *runner) handleFetchBatch(ctx context.Context, args [][]string) ( 1328 err error) { 1329 repo, _, err := r.initRepoIfNeeded(ctx, gitCmdFetch) 1330 if err != nil { 1331 return err 1332 } 1333 1334 r.log.CDebugf(ctx, "Fetching %d refs into %s", len(args), r.gitDir) 1335 1336 remote, err := repo.CreateRemote(&gogitcfg.RemoteConfig{ 1337 Name: localRepoRemoteName, 1338 URLs: []string{r.gitDir}, 1339 }) 1340 1341 var refSpecs []gogitcfg.RefSpec 1342 var deleteRefSpecs []gogitcfg.RefSpec 1343 for i, fetch := range args { 1344 if len(fetch) != 2 { 1345 return errors.Errorf("Bad fetch request: %v", fetch) 1346 } 1347 refInBareRepo := fetch[1] 1348 1349 // Push into a local ref with a temporary name, because the 1350 // git process that invoked us will get confused if we make a 1351 // ref with the same name. Later, delete this temporary ref. 1352 localTempRef := fmt.Sprintf("%s-%s-%d", 1353 plumbing.ReferenceName(refInBareRepo).Short(), r.uniqID, i) 1354 refSpec := fmt.Sprintf( 1355 "%s:refs/remotes/%s/%s", refInBareRepo, r.remote, localTempRef) 1356 r.log.CDebugf(ctx, "Fetching %s", refSpec) 1357 1358 refSpecs = append(refSpecs, gogitcfg.RefSpec(refSpec)) 1359 deleteRefSpecs = append(deleteRefSpecs, gogitcfg.RefSpec( 1360 fmt.Sprintf(":refs/remotes/%s/%s", r.remote, localTempRef))) 1361 } 1362 1363 var statusChan plumbing.StatusChan 1364 if r.verbosity >= 1 { 1365 s := make(chan plumbing.StatusUpdate) 1366 defer close(s) 1367 statusChan = plumbing.StatusChan(s) 1368 go r.processGogitStatus(ctx, s, nil) 1369 } 1370 1371 // Now "push" into the local repo to get it to store objects 1372 // from the KBFS bare repo. 1373 err = remote.PushContext(ctx, &gogit.PushOptions{ 1374 RemoteName: localRepoRemoteName, 1375 RefSpecs: refSpecs, 1376 StatusChan: statusChan, 1377 }) 1378 if err != nil && err != gogit.NoErrAlreadyUpToDate { 1379 return err 1380 } 1381 1382 // Delete the temporary refspecs now that the objects are 1383 // safely stored in the local repo. 1384 err = remote.PushContext(ctx, &gogit.PushOptions{ 1385 RemoteName: localRepoRemoteName, 1386 RefSpecs: deleteRefSpecs, 1387 }) 1388 if err != nil && err != gogit.NoErrAlreadyUpToDate { 1389 return err 1390 } 1391 1392 err = r.waitForJournal(ctx) 1393 if err != nil { 1394 return err 1395 } 1396 r.log.CDebugf(ctx, "Done waiting for journal") 1397 1398 err = r.checkGC(ctx) 1399 if err != nil { 1400 return err 1401 } 1402 1403 _, err = r.output.Write([]byte("\n")) 1404 return err 1405 } 1406 1407 // canPushAll returns true if a) the KBFS repo is currently empty, and 1408 // b) we've been asked to push all the local references (i.e., 1409 // --all/--mirror). 1410 func (r *runner) canPushAll( 1411 ctx context.Context, repo *gogit.Repository, args [][]string) ( 1412 canPushAll, kbfsRepoEmpty bool, err error) { 1413 refs, err := repo.References() 1414 if err != nil { 1415 return false, false, err 1416 } 1417 defer refs.Close() 1418 1419 // Iterate through the remote references. 1420 for { 1421 ref, err := refs.Next() 1422 if errors.Cause(err) == io.EOF { 1423 break 1424 } else if err != nil { 1425 return false, false, err 1426 } 1427 1428 if ref.Type() != plumbing.SymbolicReference { 1429 r.log.CDebugf(ctx, "Remote has at least one non-symbolic ref: %s", 1430 ref.String()) 1431 return false, false, nil 1432 } 1433 } 1434 1435 // Build a set of all the source refs that the user is pushing. 1436 sources := make(map[string]bool) 1437 for _, push := range args { 1438 if len(push) != 1 { 1439 return false, false, errors.Errorf("Bad push request: %v", push) 1440 } 1441 refspec := gogitcfg.RefSpec(push[0]) 1442 // If some ref is being pushed to a different name on the 1443 // remote, we can't do a push-all. 1444 if refspec.Src() != refspec.Dst("").String() { 1445 return false, true, nil 1446 } 1447 1448 src := refspec.Src() 1449 sources[src] = true 1450 } 1451 1452 localGit := osfs.New(r.gitDir) 1453 localStorer, err := filesystem.NewStorage(localGit) 1454 if err != nil { 1455 return false, false, err 1456 } 1457 localRefs, err := localStorer.IterReferences() 1458 if err != nil { 1459 return false, false, err 1460 } 1461 1462 // Check whether all of the local refs are being used as a source 1463 // for this push. If not, we can't blindly push everything. 1464 for { 1465 ref, err := localRefs.Next() 1466 if errors.Cause(err) == io.EOF { 1467 break 1468 } 1469 if err != nil { 1470 return false, false, err 1471 } 1472 1473 if ref.Type() == plumbing.SymbolicReference { 1474 continue 1475 } 1476 1477 // If the local repo has a non-symbolic ref that's not being 1478 // pushed, we can't push everything blindly, otherwise we 1479 // might leak some data. 1480 if !sources[ref.Name().String()] { 1481 r.log.CDebugf(ctx, "Not pushing local ref %s", ref) 1482 return false, true, nil 1483 } 1484 } 1485 1486 return true, true, nil 1487 } 1488 1489 func (r *runner) pushAll(ctx context.Context, fs *libfs.FS) (err error) { 1490 r.log.CDebugf(ctx, "Pushing the entire local repo") 1491 localFS := osfs.New(r.gitDir) 1492 1493 // First copy objects. 1494 localFSObjects, err := localFS.Chroot("objects") 1495 if err != nil { 1496 return err 1497 } 1498 fsObjects, err := fs.Chroot("objects") 1499 if err != nil { 1500 return err 1501 } 1502 1503 verb := "encrypting" 1504 if r.h.Type() == tlf.Public { 1505 verb = "signing" 1506 } 1507 1508 err = r.recursiveCopyWithCounts( 1509 ctx, localFSObjects, fsObjects, 1510 "Counting objects", "countobj", 1511 fmt.Sprintf("Preparing and %s objects", verb), "pushobj") 1512 if err != nil { 1513 return err 1514 } 1515 1516 // Hold the packed refs lock file while transferring, so we don't 1517 // clash with anyone else trying to push-init this repo. go-git 1518 // takes the same lock while writing packed-refs during a 1519 // `Remote.Fetch()` operation (used in `pushSome()` below). 1520 lockFile, err := fs.Create(packedRefsTempPath) 1521 if err != nil { 1522 return err 1523 } 1524 defer func() { 1525 closeErr := lockFile.Close() 1526 if closeErr != nil && err == nil { 1527 err = closeErr 1528 } 1529 }() 1530 1531 err = lockFile.Lock() 1532 if err != nil { 1533 return err 1534 } 1535 1536 // Next, copy refs. 1537 localFSRefs, err := localFS.Chroot("refs") 1538 if err != nil { 1539 return err 1540 } 1541 fsRefs, err := fs.Chroot("refs") 1542 if err != nil { 1543 return err 1544 } 1545 err = r.recursiveCopyWithCounts( 1546 ctx, localFSRefs, fsRefs, 1547 "Counting refs", "countref", 1548 fmt.Sprintf("Preparing and %s refs", verb), "pushref") 1549 if err != nil { 1550 return err 1551 } 1552 1553 // Finally, packed refs if it exists. 1554 _, err = localFS.Stat(packedRefsPath) 1555 if os.IsNotExist(err) { 1556 return nil 1557 } else if err != nil { 1558 return err 1559 } 1560 1561 return r.copyFileWithCount(ctx, localFS, fs, packedRefsPath, 1562 "Counting packed refs", "countprefs", 1563 fmt.Sprintf("Preparing and %s packed refs", verb), "pushprefs") 1564 } 1565 1566 func dstNameFromRefString(refStr string) plumbing.ReferenceName { 1567 return gogitcfg.RefSpec(refStr).Dst("") 1568 } 1569 1570 // parentCommitsForRef returns a map of refs with a list of commits for each 1571 // ref, newest first. It only includes commits that exist in `localStorer` but 1572 // not in `remoteStorer`. 1573 func (r *runner) parentCommitsForRef(ctx context.Context, 1574 localStorer gogitstor.Storer, remoteStorer gogitstor.Storer, 1575 refs map[gogitcfg.RefSpec]bool) (libgit.RefDataByName, error) { 1576 1577 commitsByRef := make(libgit.RefDataByName, len(refs)) 1578 haves := make(map[plumbing.Hash]bool) 1579 1580 for refspec := range refs { 1581 if refspec.IsDelete() { 1582 commitsByRef[refspec.Dst("")] = &libgit.RefData{ 1583 IsDelete: true, 1584 } 1585 continue 1586 } 1587 refName := plumbing.ReferenceName(refspec.Src()) 1588 resolved, err := gogitstor.ResolveReference(localStorer, refName) 1589 if err != nil { 1590 r.log.CDebugf(ctx, "Error resolving ref %s", refName) 1591 } 1592 if resolved != nil { 1593 refName = resolved.Name() 1594 } 1595 1596 ref, err := localStorer.Reference(refName) 1597 if err != nil { 1598 r.log.CDebugf(ctx, "Error getting reference %s: %+v", 1599 refName, err) 1600 continue 1601 } 1602 hash := ref.Hash() 1603 1604 // Get the HEAD commit for the ref from the local repository. 1605 commit, err := gogitobj.GetCommit(localStorer, hash) 1606 if err != nil { 1607 r.log.CDebugf(ctx, "Error getting commit for hash %s (%s): %+v", 1608 string(hash[:]), refName, err) 1609 continue 1610 } 1611 1612 // Iterate through the commits backward, until we experience any of the 1613 // following: 1614 // 1. Find a commit that the remote knows about, 1615 // 2. Reach our maximum number of commits to check, 1616 // 3. Run out of commits. 1617 walker := gogitobj.NewCommitPreorderIter(commit, haves, nil) 1618 toVisit := maxCommitsToVisitPerRef 1619 dstRefName := refspec.Dst("") 1620 commitsByRef[dstRefName] = &libgit.RefData{ 1621 IsDelete: refspec.IsDelete(), 1622 Commits: make([]*gogitobj.Commit, 0, maxCommitsToVisitPerRef), 1623 } 1624 err = walker.ForEach(func(c *gogitobj.Commit) error { 1625 haves[c.Hash] = true 1626 toVisit-- 1627 // If toVisit starts out at 0 (indicating there is no 1628 // max), then it will be negative here and we won't stop 1629 // early. 1630 if toVisit == 0 { 1631 // Append a sentinel value to communicate that there would be 1632 // more commits. 1633 commitsByRef[dstRefName].Commits = 1634 append(commitsByRef[dstRefName].Commits, 1635 libgit.CommitSentinelValue) 1636 return gogitstor.ErrStop 1637 } 1638 hasEncodedObjectErr := remoteStorer.HasEncodedObject(c.Hash) 1639 if hasEncodedObjectErr == nil { 1640 return gogitstor.ErrStop 1641 } 1642 commitsByRef[dstRefName].Commits = 1643 append(commitsByRef[dstRefName].Commits, c) 1644 return nil 1645 }) 1646 if err != nil { 1647 return nil, err 1648 } 1649 } 1650 return commitsByRef, nil 1651 } 1652 1653 func (r *runner) pushSome( 1654 ctx context.Context, repo *gogit.Repository, fs *libfs.FS, args [][]string, 1655 kbfsRepoEmpty bool) (map[string]error, error) { 1656 r.log.CDebugf(ctx, "Pushing %d refs into %s", len(args), r.gitDir) 1657 1658 remote, err := repo.CreateRemote(&gogitcfg.RemoteConfig{ 1659 Name: localRepoRemoteName, 1660 URLs: []string{r.gitDir}, 1661 }) 1662 if err != nil { 1663 return nil, err 1664 } 1665 1666 results := make(map[string]error, len(args)) 1667 var refspecs []gogitcfg.RefSpec 1668 refs := make(map[string]bool, len(args)) 1669 for _, push := range args { 1670 if len(push) != 1 { 1671 return nil, errors.Errorf("Bad push request: %v", push) 1672 } 1673 refspec := gogitcfg.RefSpec(push[0]) 1674 err := refspec.Validate() 1675 if err != nil { 1676 return nil, err 1677 } 1678 1679 // Delete the reference in the repo if needed; otherwise, 1680 // fetch from the local repo into the remote repo. 1681 if refspec.IsDelete() { 1682 dst := dstNameFromRefString(push[0]) 1683 if refspec.IsWildcard() { 1684 results[dst.String()] = errors.Errorf( 1685 "Wildcards not supported for deletes: %s", refspec) 1686 continue 1687 } 1688 err = repo.Storer.RemoveReference(dst) 1689 if err == gogit.NoErrAlreadyUpToDate { 1690 err = nil 1691 } 1692 results[dst.String()] = err 1693 } else { 1694 refs[refspec.Src()] = true 1695 refspecs = append(refspecs, refspec) 1696 } 1697 if err != nil { 1698 r.log.CDebugf(ctx, "Error fetching %s: %+v", refspec, err) 1699 } 1700 } 1701 1702 if len(refspecs) > 0 { 1703 var statusChan plumbing.StatusChan 1704 if r.verbosity >= 1 { 1705 s := make(chan plumbing.StatusUpdate) 1706 defer close(s) 1707 statusChan = plumbing.StatusChan(s) 1708 go func() { 1709 events := make(chan libfs.FSEvent) 1710 fs.SubscribeToEvents(events) 1711 r.processGogitStatus(ctx, s, events) 1712 fs.UnsubscribeToEvents(events) 1713 // Drain any pending writes to the channel. 1714 for range events { 1715 } 1716 }() 1717 } 1718 1719 if kbfsRepoEmpty { 1720 r.log.CDebugf( 1721 ctx, "Requesting a pack-refs file for %d refs", len(refspecs)) 1722 } 1723 1724 err = remote.FetchContext(ctx, &gogit.FetchOptions{ 1725 RemoteName: localRepoRemoteName, 1726 RefSpecs: refspecs, 1727 StatusChan: statusChan, 1728 PackRefs: kbfsRepoEmpty, 1729 }) 1730 if err == gogit.NoErrAlreadyUpToDate { 1731 err = nil 1732 } 1733 1734 // All non-deleted refspecs in the batch get the same error. 1735 for _, refspec := range refspecs { 1736 dst := refspec.Dst("") 1737 results[dst.String()] = err 1738 } 1739 } 1740 return results, nil 1741 } 1742 1743 // handlePushBatch: From https://git-scm.com/docs/git-remote-helpers 1744 // 1745 // push +<src>:<dst> 1746 // Pushes the given local <src> commit or branch to the remote branch 1747 // described by <dst>. A batch sequence of one or more push commands 1748 // is terminated with a blank line (if there is only one reference to 1749 // push, a single push command is followed by a blank line). For 1750 // example, the following would be two batches of push, the first 1751 // asking the remote-helper to push the local ref master to the remote 1752 // ref master and the local HEAD to the remote branch, and the second 1753 // asking to push ref foo to ref bar (forced update requested by the 1754 // +). 1755 // 1756 // push refs/heads/master:refs/heads/master 1757 // push HEAD:refs/heads/branch 1758 // \n 1759 // push +refs/heads/foo:refs/heads/bar 1760 // \n 1761 // 1762 // Zero or more protocol options may be entered after the last push 1763 // command, before the batch’s terminating blank line. 1764 // 1765 // When the push is complete, outputs one or more ok <dst> or error 1766 // <dst> <why>? lines to indicate success or failure of each pushed 1767 // ref. The status report output is terminated by a blank line. The 1768 // option field <why> may be quoted in a C style string if it contains 1769 // an LF. 1770 func (r *runner) handlePushBatch(ctx context.Context, args [][]string) ( 1771 commits libgit.RefDataByName, err error) { 1772 repo, fs, err := r.initRepoIfNeeded(ctx, gitCmdPush) 1773 if err != nil { 1774 return nil, err 1775 } 1776 1777 canPushAll, kbfsRepoEmpty, err := r.canPushAll(ctx, repo, args) 1778 if err != nil { 1779 return nil, err 1780 } 1781 1782 localGit := osfs.New(r.gitDir) 1783 localStorer, err := filesystem.NewStorage(localGit) 1784 if err != nil { 1785 return nil, err 1786 } 1787 1788 refspecs := make(map[gogitcfg.RefSpec]bool, len(args)) 1789 for _, push := range args { 1790 // `canPushAll` already validates the push reference. 1791 refspec := gogitcfg.RefSpec(push[0]) 1792 refspecs[refspec] = true 1793 } 1794 1795 // Get all commits associated with the refs. This must happen before the 1796 // push for us to be able to calculate the difference. 1797 commits, err = r.parentCommitsForRef(ctx, localStorer, 1798 repo.Storer, refspecs) 1799 if err != nil { 1800 return nil, err 1801 } 1802 1803 var results map[string]error 1804 // Ignore pushAll for commit collection, for now. 1805 if canPushAll { 1806 err = r.pushAll(ctx, fs) 1807 // All refs in the batch get the same error. 1808 results = make(map[string]error, len(args)) 1809 for _, push := range args { 1810 // `canPushAll` already validates the push reference. 1811 dst := dstNameFromRefString(push[0]).String() 1812 results[dst] = err 1813 } 1814 } else { 1815 results, err = r.pushSome(ctx, repo, fs, args, kbfsRepoEmpty) 1816 } 1817 if err != nil { 1818 return nil, err 1819 } 1820 1821 err = r.waitForJournal(ctx) 1822 if err != nil { 1823 return nil, err 1824 } 1825 r.log.CDebugf(ctx, "Done waiting for journal") 1826 1827 for d, e := range results { 1828 result := "" 1829 if e == nil { 1830 result = fmt.Sprintf("ok %s", d) 1831 } else { 1832 result = fmt.Sprintf("error %s %s", d, e.Error()) 1833 } 1834 _, err = r.output.Write([]byte(result + "\n")) 1835 if err != nil { 1836 return nil, err 1837 } 1838 } 1839 1840 // Remove any errored commits so that we don't send an update 1841 // message about them. 1842 for refspec := range refspecs { 1843 dst := refspec.Dst("") 1844 if results[dst.String()] != nil { 1845 r.log.CDebugf( 1846 ctx, "Removing commit result for errored push on refspec %s", 1847 refspec) 1848 delete(commits, dst) 1849 } 1850 } 1851 1852 if len(commits) > 0 { 1853 err = libgit.UpdateRepoMD(ctx, r.config, r.h, fs, 1854 keybase1.GitPushType_DEFAULT, "", commits) 1855 if err != nil { 1856 return nil, err 1857 } 1858 } 1859 1860 err = r.checkGC(ctx) 1861 if err != nil { 1862 return nil, err 1863 } 1864 1865 _, err = r.output.Write([]byte("\n")) 1866 if err != nil { 1867 return nil, err 1868 } 1869 return commits, nil 1870 } 1871 1872 // handleOption: https://git-scm.com/docs/git-remote-helpers#git-remote-helpers-emoptionemltnamegtltvaluegt 1873 // 1874 // option <name> <value> 1875 // Sets the transport helper option <name> to <value>. Outputs a 1876 // single line containing one of ok (option successfully set), 1877 // unsupported (option not recognized) or error <msg> (option <name> 1878 // is supported but <value> is not valid for it). Options should be 1879 // set before other commands, and may influence the behavior of those 1880 // commands. 1881 func (r *runner) handleOption(ctx context.Context, args []string) (err error) { 1882 defer func() { 1883 if err != nil { 1884 _, _ = r.output.Write( 1885 []byte(fmt.Sprintf("error %s\n", err.Error()))) 1886 } 1887 }() 1888 1889 if len(args) != 2 { 1890 return errors.Errorf("Bad option request: %v", args) 1891 } 1892 1893 option := args[0] 1894 result := "" 1895 switch option { 1896 case gitOptionVerbosity: 1897 v, err := strconv.ParseInt(args[1], 10, 64) 1898 if err != nil { 1899 return err 1900 } 1901 r.verbosity = v 1902 r.log.CDebugf(ctx, "Setting verbosity to %d", v) 1903 result = "ok" 1904 case gitOptionProgress: 1905 b, err := strconv.ParseBool(args[1]) 1906 if err != nil { 1907 return err 1908 } 1909 r.progress = b 1910 r.log.CDebugf(ctx, "Setting progress to %t", b) 1911 result = "ok" 1912 case gitOptionCloning: 1913 b, err := strconv.ParseBool(args[1]) 1914 if err != nil { 1915 return err 1916 } 1917 r.cloning = b 1918 r.log.CDebugf(ctx, "Setting cloning to %t", b) 1919 result = "ok" 1920 case gitOptionPushcert: 1921 if args[1] == gitOptionIfAsked { 1922 // "if-asked" means we should sign only if the server 1923 // supports it. Our server doesn't support it, but we 1924 // should still accept the configuration. 1925 result = "ok" 1926 } else { 1927 b, err := strconv.ParseBool(args[1]) 1928 if err != nil { 1929 return err 1930 } 1931 if b { 1932 // We don't support signing. 1933 r.log.CDebugf(ctx, "Signing is unsupported") 1934 result = "unsupported" 1935 } else { 1936 result = "ok" 1937 } 1938 } 1939 default: 1940 result = "unsupported" 1941 } 1942 1943 _, err = r.output.Write([]byte(result + "\n")) 1944 return err 1945 } 1946 1947 type lfsProgress struct { 1948 Event string `json:"event"` 1949 Oid string `json:"oid"` 1950 BytesSoFar int `json:"bytesSoFar"` 1951 BytesSinceLast int `json:"bytesSinceLast"` 1952 } 1953 1954 // lfsProgressWriter is a simple io.Writer shim that writes progress 1955 // messages to `r.output` for LFS. Its `printOne` function can also 1956 // be passed to `runner.waitForJournalWithPrinters` in order to print 1957 // periodic progress messages. 1958 type lfsProgressWriter struct { 1959 r *runner 1960 output io.Writer 1961 oid string 1962 start int 1963 soFar int // how much in absolute bytes has been copied 1964 totalForCopy int // how much in absolue bytes will be copied 1965 plaintextSize int // how much LFS expects to be copied 1966 factorOfPlaintextSize float64 // what frac of the above size is this copy? 1967 } 1968 1969 var _ io.Writer = (*lfsProgressWriter)(nil) 1970 1971 func (lpw *lfsProgressWriter) getProgress(newSoFar int) lfsProgress { 1972 last := lpw.soFar 1973 lpw.soFar = newSoFar 1974 1975 f := 1.0 1976 if lpw.plaintextSize > 0 { 1977 f = lpw.factorOfPlaintextSize * 1978 (float64(lpw.plaintextSize) / float64(lpw.totalForCopy)) 1979 } 1980 1981 return lfsProgress{ 1982 Event: gitLFSProgressEvent, 1983 Oid: lpw.oid, 1984 BytesSoFar: lpw.start + int(float64(lpw.soFar)*f), 1985 BytesSinceLast: int(float64(lpw.soFar-last) * f), 1986 } 1987 } 1988 1989 func (lpw *lfsProgressWriter) Write(p []byte) (n int, err error) { 1990 n, err = lpw.output.Write(p) 1991 if err != nil { 1992 return n, err 1993 } 1994 1995 if lpw.r.processType == processLFSNoProgress { 1996 return n, nil 1997 } 1998 1999 prog := lpw.getProgress(lpw.soFar + n) 2000 2001 progBytes, err := json.Marshal(prog) 2002 if err != nil { 2003 return n, err 2004 } 2005 _, err = lpw.r.output.Write(append(progBytes, []byte("\n")...)) 2006 if err != nil { 2007 return n, err 2008 } 2009 return n, nil 2010 } 2011 2012 func (lpw *lfsProgressWriter) printOne( 2013 ctx context.Context, _ int, totalSize, sizeLeft int64) int { 2014 if lpw.r.processType == processLFSNoProgress { 2015 return 0 2016 } 2017 2018 if lpw.totalForCopy == 0 { 2019 lpw.totalForCopy = int(totalSize) 2020 } 2021 2022 prog := lpw.getProgress(int(totalSize - sizeLeft)) 2023 2024 progBytes, err := json.Marshal(prog) 2025 if err != nil { 2026 lpw.r.log.CDebugf(ctx, "Error while json marshaling: %+v", err) 2027 return 0 2028 } 2029 _, err = lpw.r.output.Write(append(progBytes, []byte("\n")...)) 2030 if err != nil { 2031 lpw.r.log.CDebugf(ctx, "Error while writing: %+v", err) 2032 } 2033 return 0 2034 } 2035 2036 func (r *runner) copyFileLFS( 2037 ctx context.Context, from billy.Filesystem, to billy.Filesystem, 2038 fromName, toName, oid string, totalSize int, 2039 progressScale float64) (err error) { 2040 f, err := from.Open(fromName) 2041 if err != nil { 2042 return err 2043 } 2044 defer f.Close() 2045 toF, err := to.Create(toName) 2046 if err != nil { 2047 return err 2048 } 2049 defer toF.Close() 2050 2051 // Scale the progress by the given factor. 2052 w := &lfsProgressWriter{ 2053 r: r, 2054 oid: oid, 2055 output: toF, 2056 totalForCopy: totalSize, 2057 plaintextSize: totalSize, 2058 factorOfPlaintextSize: progressScale, 2059 } 2060 _, err = io.Copy(w, f) 2061 return err 2062 } 2063 2064 func (r *runner) handleLFSUpload( 2065 ctx context.Context, oid string, localPath string, size int) (err error) { 2066 fs, err := r.makeFS(ctx) 2067 if err != nil { 2068 return err 2069 } 2070 err = fs.MkdirAll(libgit.LFSSubdir, 0600) 2071 if err != nil { 2072 return err 2073 } 2074 fs, err = fs.ChrootAsLibFS(libgit.LFSSubdir) 2075 if err != nil { 2076 return err 2077 } 2078 2079 // Get an FS for the local directory. 2080 dir, file := filepath.Split(localPath) 2081 if dir == "" || file == "" { 2082 return errors.Errorf("Invalid local path %s", localPath) 2083 } 2084 2085 localFS := osfs.New(dir) 2086 // Have the copy count for 40% of the overall upload (arbitrary). 2087 err = r.copyFileLFS(ctx, localFS, fs, file, oid, oid, size, 0.4) 2088 if err != nil { 2089 return err 2090 } 2091 printNothing := func(_ context.Context) {} 2092 // Have the flush count for 60% of the overall upload (arbitrary). 2093 w := &lfsProgressWriter{ 2094 r: r, 2095 oid: oid, 2096 start: int(float64(size) * 0.4), 2097 plaintextSize: size, 2098 factorOfPlaintextSize: 0.60, 2099 } 2100 return r.waitForJournalWithPrinters( 2101 ctx, printNothing, w.printOne, printNothing) 2102 } 2103 2104 func (r *runner) handleLFSDownload( 2105 ctx context.Context, oid string, size int) (localPath string, err error) { 2106 fs, err := r.makeFS(ctx) 2107 if err != nil { 2108 return "", err 2109 } 2110 err = fs.MkdirAll(libgit.LFSSubdir, 0600) 2111 if err != nil { 2112 return "", err 2113 } 2114 fs, err = fs.ChrootAsLibFS(libgit.LFSSubdir) 2115 if err != nil { 2116 return "", err 2117 } 2118 2119 // Put the file in a temporary location; lfs will move it to the 2120 // right location. 2121 dir := "." 2122 localFS := osfs.New(dir) 2123 localFileName := ".kbfs_lfs_" + oid 2124 2125 err = r.copyFileLFS(ctx, fs, localFS, oid, localFileName, oid, size, 1.0) 2126 if err != nil { 2127 return "", err 2128 } 2129 return filepath.Join(dir, localFileName), nil 2130 } 2131 2132 func (r *runner) processCommand( 2133 ctx context.Context, commandChan <-chan string) (err error) { 2134 var fetchBatch, pushBatch [][]string 2135 for { 2136 select { 2137 case cmd := <-commandChan: 2138 ctx := libkbfs.CtxWithRandomIDReplayable( 2139 ctx, ctxCommandIDKey, ctxCommandOpID, r.log) 2140 2141 cmdParts := strings.Fields(cmd) 2142 if len(cmdParts) == 0 { 2143 switch { 2144 case len(fetchBatch) > 0: 2145 if r.cloning { 2146 r.log.CDebugf(ctx, "Processing clone") 2147 err = r.handleClone(ctx) 2148 if err != nil { 2149 return err 2150 } 2151 } else { 2152 r.log.CDebugf(ctx, "Processing fetch batch") 2153 err = r.handleFetchBatch(ctx, fetchBatch) 2154 if err != nil { 2155 return err 2156 } 2157 } 2158 fetchBatch = nil 2159 continue 2160 case len(pushBatch) > 0: 2161 r.log.CDebugf(ctx, "Processing push batch") 2162 _, err = r.handlePushBatch(ctx, pushBatch) 2163 if err != nil { 2164 return err 2165 } 2166 pushBatch = nil 2167 continue 2168 default: 2169 r.log.CDebugf(ctx, "Done processing commands") 2170 return nil 2171 } 2172 } 2173 2174 switch cmdParts[0] { 2175 case gitCmdCapabilities: 2176 err = r.handleCapabilities() 2177 case gitCmdList: 2178 err = r.handleList(ctx, cmdParts[1:]) 2179 case gitCmdFetch: 2180 if len(pushBatch) > 0 { 2181 return errors.New( 2182 "Cannot fetch in the middle of a push batch") 2183 } 2184 fetchBatch = append(fetchBatch, cmdParts[1:]) 2185 case gitCmdPush: 2186 if len(fetchBatch) > 0 { 2187 return errors.New( 2188 "Cannot push in the middle of a fetch batch") 2189 } 2190 pushBatch = append(pushBatch, cmdParts[1:]) 2191 case gitCmdOption: 2192 err = r.handleOption(ctx, cmdParts[1:]) 2193 default: 2194 err = errors.Errorf("Unsupported command: %s", cmdParts[0]) 2195 } 2196 if err != nil { 2197 return err 2198 } 2199 case <-ctx.Done(): 2200 return ctx.Err() 2201 } 2202 } 2203 } 2204 2205 type lfsError struct { 2206 Code int `json:"code"` 2207 Message string `json:"message"` 2208 } 2209 2210 type lfsRequest struct { 2211 Event string `json:"event"` 2212 Oid string `json:"oid"` 2213 Size int `json:"size,omitempty"` 2214 Path string `json:"path,omitempty"` 2215 Operation string `json:"operation,omitempty"` 2216 Remote string `json:"remote,omitempty"` 2217 } 2218 2219 type lfsResponse struct { 2220 Event string `json:"event"` 2221 Oid string `json:"oid"` 2222 Path string `json:"path,omitempty"` 2223 Error *lfsError `json:"error,omitempty"` 2224 } 2225 2226 func (r *runner) processCommandLFS( 2227 ctx context.Context, commandChan <-chan string) (err error) { 2228 lfsLoop: 2229 for { 2230 select { 2231 case cmd := <-commandChan: 2232 ctx := libkbfs.CtxWithRandomIDReplayable( 2233 ctx, ctxCommandIDKey, ctxCommandOpID, r.log) 2234 2235 var req lfsRequest 2236 err := json.Unmarshal([]byte(cmd), &req) 2237 if err != nil { 2238 return err 2239 } 2240 2241 resp := lfsResponse{ 2242 Event: gitLFSCompleteEvent, 2243 Oid: req.Oid, 2244 } 2245 switch req.Event { 2246 case gitLFSInitEvent: 2247 r.log.CDebugf( 2248 ctx, "Initialize message, operation=%s, remote=%s", 2249 req.Operation, req.Remote) 2250 _, err := r.output.Write([]byte("{}\n")) 2251 if err != nil { 2252 return err 2253 } 2254 continue lfsLoop 2255 case gitLFSUploadEvent: 2256 r.log.CDebugf(ctx, "Handling upload, oid=%s", req.Oid) 2257 err := r.handleLFSUpload(ctx, req.Oid, req.Path, req.Size) 2258 if err != nil { 2259 resp.Error = &lfsError{Code: 1, Message: err.Error()} 2260 } 2261 case gitLFSDownloadEvent: 2262 r.log.CDebugf(ctx, "Handling download, oid=%s", req.Oid) 2263 p, err := r.handleLFSDownload(ctx, req.Oid, req.Size) 2264 if err != nil { 2265 resp.Error = &lfsError{Code: 1, Message: err.Error()} 2266 } else { 2267 resp.Path = filepath.ToSlash(p) 2268 } 2269 case gitLFSTerminateEvent: 2270 return nil 2271 } 2272 2273 respBytes, err := json.Marshal(resp) 2274 if err != nil { 2275 return err 2276 } 2277 _, err = r.output.Write(append(respBytes, []byte("\n")...)) 2278 if err != nil { 2279 return err 2280 } 2281 case <-ctx.Done(): 2282 return ctx.Err() 2283 } 2284 } 2285 } 2286 2287 func (r *runner) processCommands(ctx context.Context) (err error) { 2288 r.log.CDebugf(ctx, "Ready to process") 2289 reader := bufio.NewReader(r.input) 2290 var wg sync.WaitGroup 2291 defer wg.Wait() 2292 ctx, cancel := context.WithCancel(ctx) 2293 defer cancel() 2294 // Allow the creation of .kbfs_git within KBFS. 2295 ctx = context.WithValue(ctx, libkbfs.CtxAllowNameKey, kbfsRepoDir) 2296 2297 // Process the commands with a separate queue in a separate 2298 // goroutine, so we can exit as soon as EOF is received 2299 // (indicating the corresponding `git` command has been 2300 // interrupted). 2301 commandChan := make(chan string, 100) 2302 processorErrChan := make(chan error, 1) 2303 wg.Add(1) 2304 go func() { 2305 defer wg.Done() 2306 switch r.processType { 2307 case processGit: 2308 processorErrChan <- r.processCommand(ctx, commandChan) 2309 case processLFS, processLFSNoProgress: 2310 processorErrChan <- r.processCommandLFS(ctx, commandChan) 2311 default: 2312 panic(fmt.Sprintf("Unknown process type: %v", r.processType)) 2313 } 2314 }() 2315 2316 for { 2317 stdinErrChan := make(chan error, 1) 2318 go func() { 2319 cmd, err := reader.ReadString('\n') 2320 if err != nil { 2321 stdinErrChan <- err 2322 return 2323 } 2324 2325 r.log.CDebugf(ctx, "Received command: %s", cmd) 2326 commandChan <- cmd 2327 stdinErrChan <- nil 2328 }() 2329 2330 select { 2331 case err := <-stdinErrChan: 2332 if errors.Cause(err) == io.EOF { 2333 r.log.CDebugf(ctx, "Done processing commands") 2334 return nil 2335 } else if err != nil { 2336 return err 2337 } 2338 // Otherwise continue to read the next command. 2339 case err := <-processorErrChan: 2340 return err 2341 case <-ctx.Done(): 2342 return ctx.Err() 2343 } 2344 } 2345 }