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  }