github.com/keybase/client/go@v0.0.0-20240520164431-4f512a4c85a3/kbfs/kbfsgit/runner_test.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  	"bytes"
     9  	"context"
    10  	"encoding/json"
    11  	"fmt"
    12  	"io"
    13  	"os"
    14  	"os/exec"
    15  	"path/filepath"
    16  	"runtime"
    17  	"strings"
    18  	"testing"
    19  
    20  	"github.com/keybase/client/go/kbfs/data"
    21  	"github.com/keybase/client/go/kbfs/libcontext"
    22  	"github.com/keybase/client/go/kbfs/libfs"
    23  	"github.com/keybase/client/go/kbfs/libgit"
    24  	"github.com/keybase/client/go/kbfs/libkbfs"
    25  	"github.com/keybase/client/go/kbfs/tlf"
    26  	"github.com/keybase/client/go/kbfs/tlfhandle"
    27  	"github.com/keybase/client/go/protocol/keybase1"
    28  	"github.com/stretchr/testify/require"
    29  	gogitcfg "gopkg.in/src-d/go-git.v4/config"
    30  )
    31  
    32  type testErrput struct {
    33  	t *testing.T
    34  }
    35  
    36  func (te testErrput) Write(buf []byte) (int, error) {
    37  	te.t.Helper()
    38  	te.t.Log(string(buf))
    39  	return 0, nil
    40  }
    41  
    42  func TestRunnerCapabilities(t *testing.T) {
    43  	ctx := libcontext.BackgroundContextWithCancellationDelayer()
    44  	config := libkbfs.MakeTestConfigOrBustLoggedInWithMode(
    45  		t, 0, libkbfs.InitSingleOp, "user1")
    46  	defer libkbfs.CheckConfigAndShutdown(ctx, t, config)
    47  
    48  	inputReader, inputWriter := io.Pipe()
    49  	defer inputWriter.Close()
    50  	go func() {
    51  		_, _ = inputWriter.Write([]byte("capabilities\n\n"))
    52  	}()
    53  
    54  	var output bytes.Buffer
    55  	r, err := newRunner(ctx, config, "origin", "keybase://private/user1/test",
    56  		"", inputReader, &output, testErrput{t})
    57  	require.NoError(t, err)
    58  	err = r.processCommands(ctx)
    59  	require.NoError(t, err)
    60  	require.Equal(t, "fetch\npush\noption\n\n", output.String())
    61  }
    62  
    63  func initConfigForRunner(t *testing.T) (
    64  	ctx context.Context, config *libkbfs.ConfigLocal, tempdir string) {
    65  	ctx = libcontext.BackgroundContextWithCancellationDelayer()
    66  	config = libkbfs.MakeTestConfigOrBustLoggedInWithMode(
    67  		t, 0, libkbfs.InitSingleOp, "user1", "user2")
    68  	success := false
    69  	ctx = context.WithValue(ctx, libkbfs.CtxAllowNameKey, kbfsRepoDir)
    70  
    71  	tempdir, err := os.MkdirTemp(os.TempDir(), "journal_server")
    72  	require.NoError(t, err)
    73  	defer func() {
    74  		if !success {
    75  			os.RemoveAll(tempdir)
    76  		}
    77  	}()
    78  
    79  	err = config.EnableDiskLimiter(tempdir)
    80  	require.NoError(t, err)
    81  	err = config.EnableJournaling(
    82  		ctx, tempdir, libkbfs.TLFJournalSingleOpBackgroundWorkEnabled)
    83  	require.NoError(t, err)
    84  
    85  	success = true
    86  	return ctx, config, tempdir
    87  }
    88  
    89  func testRunnerInitRepo(t *testing.T, tlfType tlf.Type, typeString string) {
    90  	ctx, config, tempdir := initConfigForRunner(t)
    91  	defer libkbfs.CheckConfigAndShutdown(ctx, t, config)
    92  	defer os.RemoveAll(tempdir)
    93  
    94  	inputReader, inputWriter := io.Pipe()
    95  	defer inputWriter.Close()
    96  	go func() {
    97  		_, _ = inputWriter.Write([]byte("list\n\n"))
    98  	}()
    99  
   100  	h, err := tlfhandle.ParseHandle(
   101  		ctx, config.KBPKI(), config.MDOps(), config, "user1", tlfType)
   102  	require.NoError(t, err)
   103  	if tlfType != tlf.Public {
   104  		_, err = libgit.CreateRepoAndID(ctx, config, h, "test")
   105  		require.NoError(t, err)
   106  	}
   107  
   108  	var output bytes.Buffer
   109  	r, err := newRunner(ctx, config, "origin",
   110  		fmt.Sprintf("keybase://%s/user1/test", typeString),
   111  		"", inputReader, &output, testErrput{t})
   112  	require.NoError(t, err)
   113  	err = r.processCommands(ctx)
   114  	require.NoError(t, err)
   115  	// No refs yet, including the HEAD symref.
   116  	require.Equal(t, output.String(), "\n")
   117  
   118  	// Now there should be a valid git repo stored in KBFS.  Check the
   119  	// existence of the HEAD file to be sure.
   120  	fs, err := libfs.NewFS(
   121  		ctx, config, h, data.MasterBranch, ".kbfs_git/test", "",
   122  		keybase1.MDPriorityGit)
   123  	require.NoError(t, err)
   124  	head, err := fs.Open("HEAD")
   125  	require.NoError(t, err)
   126  	buf, err := io.ReadAll(head)
   127  	require.NoError(t, err)
   128  	require.Equal(t, "ref: refs/heads/master\n", string(buf))
   129  }
   130  
   131  func TestRunnerInitRepoPrivate(t *testing.T) {
   132  	testRunnerInitRepo(t, tlf.Private, "private")
   133  }
   134  
   135  func TestRunnerInitRepoPublic(t *testing.T) {
   136  	testRunnerInitRepo(t, tlf.Public, "public")
   137  }
   138  
   139  func gitExec(t *testing.T, gitDir, workTree string, command ...string) {
   140  	cmd := exec.Command("git",
   141  		append([]string{"--git-dir", gitDir, "--work-tree", workTree},
   142  			command...)...)
   143  	output, err := cmd.CombinedOutput()
   144  	require.NoError(t, err, string(output))
   145  }
   146  
   147  func makeLocalRepoWithOneFileCustomCommitMsg(t *testing.T,
   148  	gitDir, filename, contents, branch, msg string) {
   149  	t.Logf("Make a new repo in %s with one file", gitDir)
   150  	err := os.WriteFile(
   151  		filepath.Join(gitDir, filename), []byte(contents), 0600)
   152  	require.NoError(t, err)
   153  	dotgit := filepath.Join(gitDir, ".git")
   154  	gitExec(t, dotgit, gitDir, "init")
   155  
   156  	if branch != "" {
   157  		gitExec(t, dotgit, gitDir, "checkout", "-b", branch)
   158  	}
   159  
   160  	gitExec(t, dotgit, gitDir, "add", filename)
   161  	gitExec(t, dotgit, gitDir, "-c", "user.name=Foo",
   162  		"-c", "user.email=foo@foo.com", "commit", "-a", "-m", msg)
   163  }
   164  
   165  func makeLocalRepoWithOneFile(t *testing.T,
   166  	gitDir, filename, contents, branch string) {
   167  	makeLocalRepoWithOneFileCustomCommitMsg(
   168  		t, gitDir, filename, contents, branch, "foo")
   169  }
   170  
   171  func addOneFileToRepoCustomCommitMsg(t *testing.T, gitDir,
   172  	filename, contents, msg string) {
   173  	t.Logf("Add a new file to %s", gitDir)
   174  	err := os.WriteFile(
   175  		filepath.Join(gitDir, filename), []byte(contents), 0600)
   176  	require.NoError(t, err)
   177  	dotgit := filepath.Join(gitDir, ".git")
   178  
   179  	gitExec(t, dotgit, gitDir, "add", filename)
   180  	gitExec(t, dotgit, gitDir, "-c", "user.name=Foo",
   181  		"-c", "user.email=foo@foo.com", "commit", "-a", "-m", msg)
   182  }
   183  
   184  func addOneFileToRepo(t *testing.T, gitDir, filename, contents string) {
   185  	addOneFileToRepoCustomCommitMsg(
   186  		t, gitDir, filename, contents, "foo")
   187  }
   188  
   189  func testPushWithTemplate(ctx context.Context, t *testing.T,
   190  	config libkbfs.Config, gitDir string, refspecs []string,
   191  	outputTemplate, tlfName string) {
   192  	// Use the runner to push the local data into the KBFS repo.
   193  	inputReader, inputWriter := io.Pipe()
   194  	defer inputWriter.Close()
   195  	go func() {
   196  		for _, refspec := range refspecs {
   197  			_, _ = inputWriter.Write([]byte(fmt.Sprintf("push %s\n", refspec)))
   198  		}
   199  		_, _ = inputWriter.Write([]byte("\n\n"))
   200  	}()
   201  
   202  	var output bytes.Buffer
   203  	r, err := newRunner(ctx, config, "origin",
   204  		fmt.Sprintf("keybase://private/%s/test", tlfName),
   205  		filepath.Join(gitDir, ".git"), inputReader, &output, testErrput{t})
   206  	require.NoError(t, err)
   207  	err = r.processCommands(ctx)
   208  	require.NoError(t, err)
   209  
   210  	// The output can list refs in any order, so we need to compare
   211  	// maps rather than raw strings.
   212  	outputLines := strings.Split(output.String(), "\n")
   213  	outputMap := make(map[string]bool)
   214  	for _, line := range outputLines {
   215  		outputMap[line] = true
   216  	}
   217  
   218  	dsts := make([]interface{}, 0, len(refspecs))
   219  	for _, refspec := range refspecs {
   220  		dsts = append(dsts, gogitcfg.RefSpec(refspec).Dst(""))
   221  	}
   222  	expectedOutput := fmt.Sprintf(outputTemplate, dsts...)
   223  	expectedOutputLines := strings.Split(expectedOutput, "\n")
   224  	expectedOutputMap := make(map[string]bool)
   225  	for _, line := range expectedOutputLines {
   226  		expectedOutputMap[line] = true
   227  	}
   228  
   229  	require.Equal(t, expectedOutputMap, outputMap)
   230  }
   231  
   232  func testPush(ctx context.Context, t *testing.T, config libkbfs.Config,
   233  	gitDir, refspec string) {
   234  	testPushWithTemplate(ctx, t, config, gitDir, []string{refspec},
   235  		"ok %s\n\n", "user1")
   236  }
   237  
   238  func testListAndGetHeadsWithNameWithPush(
   239  	ctx context.Context, t *testing.T, config libkbfs.Config, gitDir string,
   240  	expectedRefs []string, tlfName string, forPush bool) (heads []string) {
   241  	inputReader, inputWriter := io.Pipe()
   242  	defer inputWriter.Close()
   243  	go func() {
   244  		p := ""
   245  		if forPush {
   246  			p = " for-push"
   247  		}
   248  		_, _ = inputWriter.Write([]byte(fmt.Sprintf("list%s\n\n", p)))
   249  	}()
   250  
   251  	var output bytes.Buffer
   252  	r, err := newRunner(ctx, config, "origin",
   253  		fmt.Sprintf("keybase://private/%s/test", tlfName),
   254  		filepath.Join(gitDir, ".git"), inputReader, &output, testErrput{t})
   255  	require.NoError(t, err)
   256  	err = r.processCommands(ctx)
   257  	require.NoError(t, err)
   258  	listLines := strings.Split(output.String(), "\n")
   259  	t.Log(listLines)
   260  	require.Len(t, listLines, len(expectedRefs)+2 /* extra blank line */)
   261  	refs := make(map[string]string, len(expectedRefs))
   262  	for _, line := range listLines {
   263  		if line == "" {
   264  			continue
   265  		}
   266  		refParts := strings.Split(line, " ")
   267  		require.Len(t, refParts, 2)
   268  		refs[refParts[1]] = refParts[0]
   269  	}
   270  
   271  	for _, expectedRef := range expectedRefs {
   272  		head, ok := refs[expectedRef]
   273  		require.True(t, ok)
   274  		heads = append(heads, head)
   275  	}
   276  	return heads
   277  }
   278  
   279  func testListAndGetHeadsWithName(ctx context.Context, t *testing.T,
   280  	config libkbfs.Config, gitDir string, expectedRefs []string,
   281  	tlfName string) (heads []string) {
   282  	return testListAndGetHeadsWithNameWithPush(
   283  		ctx, t, config, gitDir, expectedRefs, tlfName, false)
   284  }
   285  
   286  func testListAndGetHeads(ctx context.Context, t *testing.T,
   287  	config libkbfs.Config, gitDir string, expectedRefs []string) (
   288  	heads []string) {
   289  	return testListAndGetHeadsWithName(
   290  		ctx, t, config, gitDir, expectedRefs, "user1")
   291  }
   292  
   293  // This tests pushing code to a bare repo stored in KBFS, and pulling
   294  // code from that bare repo into a new working tree.  This is a simple
   295  // version of how the full KBFS Git system will work.  Specifically,
   296  // this test does the following:
   297  //
   298  // 1) Initializes a new repo on the local file system with one file.
   299  // 2) Initializes a new bare repo in KBFS.
   300  // 3) User pushes from that repo into the remote KBFS repo.
   301  // 4) Initializes a second new repo on the local file system.
   302  // 5) User pulls from the remote KBFS repo into the second repo.
   303  func testRunnerPushFetch(t *testing.T, cloning bool, secondRepoHasBranch bool) {
   304  	ctx, config, tempdir := initConfigForRunner(t)
   305  	defer os.RemoveAll(tempdir)
   306  	defer libkbfs.CheckConfigAndShutdown(ctx, t, config)
   307  
   308  	git1, err := os.MkdirTemp(os.TempDir(), "kbfsgittest")
   309  	require.NoError(t, err)
   310  	defer os.RemoveAll(git1)
   311  
   312  	makeLocalRepoWithOneFile(t, git1, "foo", "hello", "")
   313  
   314  	h, err := tlfhandle.ParseHandle(
   315  		ctx, config.KBPKI(), config.MDOps(), config, "user1", tlf.Private)
   316  	require.NoError(t, err)
   317  	_, err = libgit.CreateRepoAndID(ctx, config, h, "test")
   318  	require.NoError(t, err)
   319  
   320  	testPush(ctx, t, config, git1, "refs/heads/master:refs/heads/master")
   321  
   322  	git2, err := os.MkdirTemp(os.TempDir(), "kbfsgittest")
   323  	require.NoError(t, err)
   324  	defer os.RemoveAll(git2)
   325  
   326  	t.Logf("Make a new repo in %s to clone from the KBFS repo", git2)
   327  	dotgit2 := filepath.Join(git2, ".git")
   328  	gitExec(t, dotgit2, git2, "init")
   329  
   330  	// Find out the head hash.
   331  	heads := testListAndGetHeads(ctx, t, config, git2,
   332  		[]string{"refs/heads/master", "HEAD"})
   333  
   334  	cloningStr := ""
   335  	cloningRetStr := ""
   336  	if cloning {
   337  		cloningStr = "option cloning true\n"
   338  		cloningRetStr = "ok\n"
   339  	} else if secondRepoHasBranch {
   340  		makeLocalRepoWithOneFile(t, git2, "foo2", "hello2", "b")
   341  	}
   342  
   343  	// Use the runner to fetch the KBFS data into the new git repo.
   344  	inputReader, inputWriter := io.Pipe()
   345  	defer inputWriter.Close()
   346  	go func() {
   347  		_, _ = inputWriter.Write([]byte(fmt.Sprintf(
   348  			"%sfetch %s refs/heads/master\n\n\n", cloningStr, heads[0])))
   349  	}()
   350  
   351  	var output3 bytes.Buffer
   352  	r, err := newRunner(ctx, config, "origin", "keybase://private/user1/test",
   353  		dotgit2, inputReader, &output3, testErrput{t})
   354  	require.NoError(t, err)
   355  	err = r.processCommands(ctx)
   356  	require.NoError(t, err)
   357  	// Just one symref, from HEAD to master (and master has no commits yet).
   358  	require.Equal(t, cloningRetStr+"\n", output3.String())
   359  
   360  	// Checkout the head directly (fetching directly via the runner
   361  	// doesn't leave any refs, those would normally be created by the
   362  	// `git` process that invokes the runner).
   363  	gitExec(t, dotgit2, git2, "checkout", heads[0])
   364  
   365  	data, err := os.ReadFile(filepath.Join(git2, "foo"))
   366  	require.NoError(t, err)
   367  	require.Equal(t, "hello", string(data))
   368  }
   369  
   370  func TestRunnerPushFetch(t *testing.T) {
   371  	t.Skip("KBFS-3778: currently flaking")
   372  	testRunnerPushFetch(t, false, false)
   373  }
   374  
   375  func TestRunnerPushClone(t *testing.T) {
   376  	testRunnerPushFetch(t, true, false)
   377  }
   378  
   379  func TestRunnerPushFetchWithBranch(t *testing.T) {
   380  	t.Skip("KBFS-3589: currently flaking")
   381  	testRunnerPushFetch(t, false, true)
   382  }
   383  
   384  func TestRunnerListForPush(t *testing.T) {
   385  	ctx, config, tempdir := initConfigForRunner(t)
   386  	defer os.RemoveAll(tempdir)
   387  	defer libkbfs.CheckConfigAndShutdown(ctx, t, config)
   388  
   389  	git1, err := os.MkdirTemp(os.TempDir(), "kbfsgittest")
   390  	require.NoError(t, err)
   391  	defer os.RemoveAll(git1)
   392  
   393  	makeLocalRepoWithOneFile(t, git1, "foo", "hello", "")
   394  
   395  	h, err := tlfhandle.ParseHandle(
   396  		ctx, config.KBPKI(), config.MDOps(), config, "user1", tlf.Private)
   397  	require.NoError(t, err)
   398  	_, err = libgit.CreateRepoAndID(ctx, config, h, "test")
   399  	require.NoError(t, err)
   400  
   401  	testPush(ctx, t, config, git1, "refs/heads/master:refs/heads/master")
   402  
   403  	// Make sure we don't list symbolic references (see KBFS-1970).
   404  	_ = testListAndGetHeadsWithNameWithPush(
   405  		ctx, t, config, git1, []string{"refs/heads/master"}, "user1", true)
   406  }
   407  
   408  func TestRunnerDeleteBranch(t *testing.T) {
   409  	ctx, config, tempdir := initConfigForRunner(t)
   410  	defer os.RemoveAll(tempdir)
   411  	defer libkbfs.CheckConfigAndShutdown(ctx, t, config)
   412  
   413  	git, err := os.MkdirTemp(os.TempDir(), "kbfsgittest")
   414  	require.NoError(t, err)
   415  	defer os.RemoveAll(git)
   416  
   417  	makeLocalRepoWithOneFile(t, git, "foo", "hello", "")
   418  
   419  	h, err := tlfhandle.ParseHandle(
   420  		ctx, config.KBPKI(), config.MDOps(), config, "user1", tlf.Private)
   421  	require.NoError(t, err)
   422  	_, err = libgit.CreateRepoAndID(ctx, config, h, "test")
   423  	require.NoError(t, err)
   424  
   425  	testPush(ctx, t, config, git, "refs/heads/master:refs/heads/master")
   426  	testPush(ctx, t, config, git, "refs/heads/master:refs/heads/test")
   427  
   428  	// Make sure there are 2 remote branches.
   429  	testListAndGetHeads(ctx, t, config, git,
   430  		[]string{"refs/heads/master", "refs/heads/test", "HEAD"})
   431  
   432  	// Delete the test branch and make sure it goes away.
   433  	testPush(ctx, t, config, git, ":refs/heads/test")
   434  	testListAndGetHeads(ctx, t, config, git,
   435  		[]string{"refs/heads/master", "HEAD"})
   436  }
   437  
   438  func TestRunnerExitEarlyOnEOF(t *testing.T) {
   439  	ctx, config, tempdir := initConfigForRunner(t)
   440  	defer os.RemoveAll(tempdir)
   441  	defer libkbfs.CheckConfigAndShutdown(ctx, t, config)
   442  
   443  	git, err := os.MkdirTemp(os.TempDir(), "kbfsgittest")
   444  	require.NoError(t, err)
   445  	defer os.RemoveAll(git)
   446  
   447  	makeLocalRepoWithOneFile(t, git, "foo", "hello", "")
   448  
   449  	h, err := tlfhandle.ParseHandle(
   450  		ctx, config.KBPKI(), config.MDOps(), config, "user1", tlf.Private)
   451  	require.NoError(t, err)
   452  	rootNode, _, err := config.KBFSOps().GetOrCreateRootNode(
   453  		ctx, h, data.MasterBranch)
   454  	require.NoError(t, err)
   455  	_, err = libgit.CreateRepoAndID(ctx, config, h, "test")
   456  	require.NoError(t, err)
   457  
   458  	// Pause journal to force the processing to pause.
   459  	jManager, err := libkbfs.GetJournalManager(config)
   460  	require.NoError(t, err)
   461  	jManager.PauseBackgroundWork(ctx, rootNode.GetFolderBranch().Tlf)
   462  
   463  	// Input a full push batch, but let the reader EOF without giving
   464  	// the final \n.
   465  	input := bytes.NewBufferString(
   466  		"push refs/heads/master:refs/heads/master\n\n")
   467  	var output bytes.Buffer
   468  	r, err := newRunner(ctx, config, "origin", "keybase://private/user1/test",
   469  		filepath.Join(git, ".git"), input, &output, testErrput{t})
   470  	require.NoError(t, err)
   471  
   472  	// Make sure we don't hang when EOF comes early.
   473  	err = r.processCommands(ctx)
   474  	require.NoError(t, err)
   475  
   476  	err = config.KBFSOps().SyncAll(ctx, rootNode.GetFolderBranch())
   477  	require.NoError(t, err)
   478  }
   479  
   480  func TestForcePush(t *testing.T) {
   481  	ctx, config, tempdir := initConfigForRunner(t)
   482  	defer os.RemoveAll(tempdir)
   483  	defer libkbfs.CheckConfigAndShutdown(ctx, t, config)
   484  
   485  	git, err := os.MkdirTemp(os.TempDir(), "kbfsgittest")
   486  	require.NoError(t, err)
   487  	defer os.RemoveAll(git)
   488  
   489  	makeLocalRepoWithOneFile(t, git, "foo", "hello", "")
   490  
   491  	h, err := tlfhandle.ParseHandle(
   492  		ctx, config.KBPKI(), config.MDOps(), config, "user1", tlf.Private)
   493  	require.NoError(t, err)
   494  	_, err = libgit.CreateRepoAndID(ctx, config, h, "test")
   495  	require.NoError(t, err)
   496  
   497  	testPush(ctx, t, config, git, "refs/heads/master:refs/heads/master")
   498  
   499  	// Push a second file.
   500  	addOneFileToRepo(t, git, "foo2", "hello2")
   501  	testPush(ctx, t, config, git, "refs/heads/master:refs/heads/master")
   502  
   503  	// Now revert to the old commit and add a different file.
   504  	dotgit := filepath.Join(git, ".git")
   505  	gitExec(t, dotgit, git, "reset", "--hard", "HEAD~1")
   506  
   507  	addOneFileToRepo(t, git, "foo3", "hello3")
   508  	// A non-force push should fail.
   509  	testPushWithTemplate(
   510  		ctx, t, config, git, []string{"refs/heads/master:refs/heads/master"},
   511  		"error %s some refs were not updated\n\n", "user1")
   512  	// But a force push should work
   513  	testPush(ctx, t, config, git, "+refs/heads/master:refs/heads/master")
   514  }
   515  
   516  func TestPushAllWithPackedRefs(t *testing.T) {
   517  	ctx, config, tempdir := initConfigForRunner(t)
   518  	defer os.RemoveAll(tempdir)
   519  	defer libkbfs.CheckConfigAndShutdown(ctx, t, config)
   520  
   521  	git, err := os.MkdirTemp(os.TempDir(), "kbfsgittest")
   522  	require.NoError(t, err)
   523  	defer os.RemoveAll(git)
   524  
   525  	makeLocalRepoWithOneFile(t, git, "foo", "hello", "")
   526  
   527  	dotgit := filepath.Join(git, ".git")
   528  	gitExec(t, dotgit, git, "pack-refs", "--all")
   529  
   530  	h, err := tlfhandle.ParseHandle(
   531  		ctx, config.KBPKI(), config.MDOps(), config, "user1", tlf.Private)
   532  	require.NoError(t, err)
   533  	_, err = libgit.CreateRepoAndID(ctx, config, h, "test")
   534  	require.NoError(t, err)
   535  
   536  	testPush(ctx, t, config, git, "refs/heads/master:refs/heads/master")
   537  
   538  	// Should be able to update the branch in a non-force way, even
   539  	// though it's a packed-ref.
   540  	addOneFileToRepo(t, git, "foo2", "hello2")
   541  	testPush(ctx, t, config, git, "refs/heads/master:refs/heads/master")
   542  }
   543  
   544  func TestPushSomeWithPackedRefs(t *testing.T) {
   545  	ctx, config, tempdir := initConfigForRunner(t)
   546  	defer os.RemoveAll(tempdir)
   547  	defer libkbfs.CheckConfigAndShutdown(ctx, t, config)
   548  
   549  	git, err := os.MkdirTemp(os.TempDir(), "kbfsgittest")
   550  	require.NoError(t, err)
   551  	defer os.RemoveAll(git)
   552  
   553  	h, err := tlfhandle.ParseHandle(
   554  		ctx, config.KBPKI(), config.MDOps(), config, "user1", tlf.Private)
   555  	require.NoError(t, err)
   556  	_, err = libgit.CreateRepoAndID(ctx, config, h, "test")
   557  	require.NoError(t, err)
   558  
   559  	makeLocalRepoWithOneFile(t, git, "foo", "hello", "")
   560  
   561  	// Make a non-branch ref (under refs/test).  This ref would not be
   562  	// pushed as part of `git push --all`.
   563  	dotgit := filepath.Join(git, ".git")
   564  	gitExec(t, dotgit, git, "push", git, "HEAD:refs/test/ref")
   565  
   566  	addOneFileToRepo(t, git, "foo2", "hello2")
   567  
   568  	// Make a tag, and then another branch.
   569  	gitExec(t, dotgit, git, "tag", "v0")
   570  	gitExec(t, dotgit, git, "checkout", "-b", "test")
   571  	addOneFileToRepo(t, git, "foo3", "hello3")
   572  
   573  	// Simulate a `git push --all`, and make sure `refs/test/ref`
   574  	// isn't pushed.
   575  	testPushWithTemplate(
   576  		ctx, t, config, git, []string{
   577  			"refs/heads/master:refs/heads/master",
   578  			"refs/heads/test:refs/heads/test",
   579  			"refs/tags/v0:refs/tags/v0",
   580  		},
   581  		"ok %s\nok %s\nok %s\n\n", "user1")
   582  	testListAndGetHeads(ctx, t, config, git,
   583  		[]string{
   584  			"refs/heads/master",
   585  			"refs/heads/test",
   586  			"refs/tags/v0",
   587  			"HEAD",
   588  		})
   589  
   590  	// Make sure we can push over a packed-refs ref.
   591  	addOneFileToRepo(t, git, "foo4", "hello4")
   592  	testPush(ctx, t, config, git, "refs/heads/test:refs/heads/test")
   593  }
   594  
   595  func testCloneIntoNewLocalRepo(
   596  	ctx context.Context, t *testing.T, config libkbfs.Config,
   597  	tlfName string) string {
   598  	git, err := os.MkdirTemp(os.TempDir(), "kbfsgittest")
   599  	require.NoError(t, err)
   600  	success := false
   601  	defer func() {
   602  		if !success {
   603  			os.RemoveAll(git)
   604  		}
   605  	}()
   606  
   607  	dotgit := filepath.Join(git, ".git")
   608  	gitExec(t, dotgit, git, "init")
   609  
   610  	heads := testListAndGetHeadsWithName(ctx, t, config, git,
   611  		[]string{"refs/heads/master", "HEAD"}, tlfName)
   612  
   613  	inputReader, inputWriter := io.Pipe()
   614  	defer inputWriter.Close()
   615  	go func() {
   616  		_, _ = inputWriter.Write([]byte(fmt.Sprintf(
   617  			"option cloning true\n"+
   618  				"fetch %s refs/heads/master\n\n\n", heads[0])))
   619  	}()
   620  
   621  	var output bytes.Buffer
   622  	r2, err := newRunner(ctx, config, "origin",
   623  		fmt.Sprintf("keybase://private/%s/test", tlfName),
   624  		dotgit, inputReader, &output, testErrput{t})
   625  	require.NoError(t, err)
   626  	err = r2.processCommands(ctx)
   627  	require.NoError(t, err)
   628  	// Just one symref, from HEAD to master (and master has no commits yet).
   629  	require.Equal(t, "ok\n\n", output.String())
   630  
   631  	// Checkout the head directly (fetching directly via the runner
   632  	// doesn't leave any refs, those would normally be created by the
   633  	// `git` process that invokes the runner).
   634  	gitExec(t, dotgit, git, "checkout", heads[0])
   635  
   636  	success = true
   637  	return git
   638  }
   639  
   640  func TestRunnerReaderClone(t *testing.T) {
   641  	ctx, config, tempdir := initConfigForRunner(t)
   642  	defer os.RemoveAll(tempdir)
   643  	defer libkbfs.CheckConfigAndShutdown(ctx, t, config)
   644  
   645  	git1, err := os.MkdirTemp(os.TempDir(), "kbfsgittest")
   646  	require.NoError(t, err)
   647  	defer os.RemoveAll(git1)
   648  
   649  	makeLocalRepoWithOneFile(t, git1, "foo", "hello", "")
   650  	testPushWithTemplate(ctx, t, config, git1,
   651  		[]string{"refs/heads/master:refs/heads/master"},
   652  		"ok %s\n\n", "user1#user2")
   653  
   654  	// Make sure the reader can clone it.
   655  	tempdir2, err := os.MkdirTemp(os.TempDir(), "journal_server")
   656  	require.NoError(t, err)
   657  	defer os.RemoveAll(tempdir2)
   658  	config2 := libkbfs.ConfigAsUser(config, "user2")
   659  	defer libkbfs.CheckConfigAndShutdown(ctx, t, config2)
   660  	err = config2.EnableDiskLimiter(tempdir2)
   661  	require.NoError(t, err)
   662  	err = config2.EnableJournaling(
   663  		ctx, tempdir2, libkbfs.TLFJournalSingleOpBackgroundWorkEnabled)
   664  	require.NoError(t, err)
   665  
   666  	git2 := testCloneIntoNewLocalRepo(ctx, t, config2, "user1#user2")
   667  	defer os.RemoveAll(git2)
   668  
   669  	data, err := os.ReadFile(filepath.Join(git2, "foo"))
   670  	require.NoError(t, err)
   671  	require.Equal(t, "hello", string(data))
   672  }
   673  
   674  func TestRunnerDeletePackedRef(t *testing.T) {
   675  	ctx, config, tempdir := initConfigForRunner(t)
   676  	defer os.RemoveAll(tempdir)
   677  	defer libkbfs.CheckConfigAndShutdown(ctx, t, config)
   678  
   679  	git1, err := os.MkdirTemp(os.TempDir(), "kbfsgittest")
   680  	require.NoError(t, err)
   681  	defer os.RemoveAll(git1)
   682  	dotgit1 := filepath.Join(git1, ".git")
   683  
   684  	makeLocalRepoWithOneFile(t, git1, "foo", "hello", "b")
   685  
   686  	// Add a different file to master.
   687  	gitExec(t, dotgit1, git1, "checkout", "-b", "master")
   688  	addOneFileToRepo(t, git1, "foo2", "hello2")
   689  
   690  	gitExec(t, dotgit1, git1, "pack-refs", "--all")
   691  
   692  	h, err := tlfhandle.ParseHandle(
   693  		ctx, config.KBPKI(), config.MDOps(), config, "user1", tlf.Private)
   694  	require.NoError(t, err)
   695  	_, err = libgit.CreateRepoAndID(ctx, config, h, "test")
   696  	require.NoError(t, err)
   697  
   698  	testPushWithTemplate(
   699  		ctx, t, config, git1, []string{
   700  			"refs/heads/master:refs/heads/master",
   701  			"refs/heads/b:refs/heads/b",
   702  		},
   703  		"ok %s\nok %s\n\n", "user1")
   704  
   705  	testListAndGetHeadsWithName(ctx, t, config, git1,
   706  		[]string{"refs/heads/master", "refs/heads/b", "HEAD"}, "user1")
   707  
   708  	// Add a new file to the branch and push, to create a loose ref.
   709  	gitExec(t, dotgit1, git1, "checkout", "b")
   710  	addOneFileToRepo(t, git1, "foo3", "hello3")
   711  	testPush(ctx, t, config, git1, "refs/heads/b:refs/heads/b")
   712  
   713  	// Now delete.
   714  	testPush(ctx, t, config, git1, ":refs/heads/b")
   715  	testListAndGetHeadsWithName(ctx, t, config, git1,
   716  		[]string{"refs/heads/master", "HEAD"}, "user1")
   717  }
   718  
   719  func TestPushcertOptions(t *testing.T) {
   720  	ctx, config, tempdir := initConfigForRunner(t)
   721  	defer os.RemoveAll(tempdir)
   722  	defer libkbfs.CheckConfigAndShutdown(ctx, t, config)
   723  
   724  	git, err := os.MkdirTemp(os.TempDir(), "kbfsgittest")
   725  	require.NoError(t, err)
   726  	defer os.RemoveAll(git)
   727  	dotgit := filepath.Join(git, ".git")
   728  
   729  	checkPushcert := func(option, expected string) {
   730  		inputReader, inputWriter := io.Pipe()
   731  		defer inputWriter.Close()
   732  		go func() {
   733  			_, _ = inputWriter.Write([]byte(fmt.Sprintf(
   734  				"option pushcert %s\n\n", option)))
   735  		}()
   736  
   737  		var output bytes.Buffer
   738  		r, err := newRunner(ctx, config, "origin",
   739  			"keybase://private/user1/test",
   740  			dotgit, inputReader, &output, testErrput{t})
   741  		require.NoError(t, err)
   742  		err = r.processCommands(ctx)
   743  		require.NoError(t, err)
   744  		// if-asked is supported, but signing will never be asked for.
   745  		require.Equal(t, fmt.Sprintf("%s\n", expected), output.String())
   746  	}
   747  
   748  	checkPushcert("if-asked", "ok")
   749  	checkPushcert("true", "unsupported")
   750  	checkPushcert("false", "ok")
   751  }
   752  
   753  func TestPackRefsAndOverwritePackedRef(t *testing.T) {
   754  	ctx, config, tempdir := initConfigForRunner(t)
   755  	defer os.RemoveAll(tempdir)
   756  	defer libkbfs.CheckConfigAndShutdown(ctx, t, config)
   757  
   758  	git1, err := os.MkdirTemp(os.TempDir(), "kbfsgittest")
   759  	require.NoError(t, err)
   760  	defer os.RemoveAll(git1)
   761  
   762  	// Make shared repo with 2 branches.
   763  	makeLocalRepoWithOneFile(t, git1, "foo", "hello", "")
   764  	testPushWithTemplate(ctx, t, config, git1,
   765  		[]string{"refs/heads/master:refs/heads/master"},
   766  		"ok %s\n\n", "user1,user2")
   767  	testPushWithTemplate(ctx, t, config, git1,
   768  		[]string{"refs/heads/master:refs/heads/test"},
   769  		"ok %s\n\n", "user1,user2")
   770  
   771  	// Config for the second user.
   772  	tempdir2, err := os.MkdirTemp(os.TempDir(), "journal_server")
   773  	require.NoError(t, err)
   774  	defer os.RemoveAll(tempdir2)
   775  	config2 := libkbfs.ConfigAsUser(config, "user2")
   776  	defer libkbfs.CheckConfigAndShutdown(ctx, t, config2)
   777  	err = config2.EnableDiskLimiter(tempdir2)
   778  	require.NoError(t, err)
   779  	err = config2.EnableJournaling(
   780  		ctx, tempdir2, libkbfs.TLFJournalSingleOpBackgroundWorkEnabled)
   781  	require.NoError(t, err)
   782  
   783  	git2, err := os.MkdirTemp(os.TempDir(), "kbfsgittest")
   784  	require.NoError(t, err)
   785  	defer os.RemoveAll(git2)
   786  
   787  	heads := testListAndGetHeadsWithName(ctx, t, config2, git2,
   788  		[]string{"refs/heads/master", "refs/heads/test", "HEAD"}, "user1,user2")
   789  	require.Equal(t, heads[0], heads[1])
   790  
   791  	// Have the second user refpack, but stall it after it takes the lock.
   792  	packOnStalled, packUnstall, packCtx := libkbfs.StallMDOp(
   793  		ctx, config2, libkbfs.StallableMDAfterGetRange, 1)
   794  	packErrCh := make(chan error)
   795  	h, err := tlfhandle.ParseHandle(
   796  		ctx, config2.KBPKI(), config.MDOps(), config, "user1,user2",
   797  		tlf.Private)
   798  	require.NoError(t, err)
   799  	go func() {
   800  		packErrCh <- libgit.GCRepo(
   801  			packCtx, config2, h, "test", libgit.GCOptions{
   802  				MaxLooseRefs:   0,
   803  				MaxObjectPacks: -1,
   804  			})
   805  	}()
   806  	select {
   807  	case <-packOnStalled:
   808  	case <-ctx.Done():
   809  		t.Fatal(ctx.Err())
   810  	}
   811  
   812  	// While the second user is stalled, have the first user update
   813  	// one of the refs.
   814  	addOneFileToRepo(t, git1, "foo2", "hello2")
   815  	testPushWithTemplate(ctx, t, config, git1,
   816  		[]string{"refs/heads/master:refs/heads/test"},
   817  		"ok %s\n\n", "user1,user2")
   818  
   819  	close(packUnstall)
   820  	select {
   821  	case err := <-packErrCh:
   822  		require.NoError(t, err)
   823  	case <-ctx.Done():
   824  		t.Fatal(ctx.Err())
   825  	}
   826  
   827  	rootNode, _, err := config2.KBFSOps().GetOrCreateRootNode(
   828  		ctx, h, data.MasterBranch)
   829  	require.NoError(t, err)
   830  	err = config2.KBFSOps().SyncFromServer(
   831  		ctx, rootNode.GetFolderBranch(), nil)
   832  	require.NoError(t, err)
   833  	heads = testListAndGetHeadsWithName(ctx, t, config2, git2,
   834  		[]string{"refs/heads/master", "refs/heads/test", "HEAD"}, "user1,user2")
   835  	require.NotEqual(t, heads[0], heads[1])
   836  }
   837  
   838  func TestPackRefsAndDeletePackedRef(t *testing.T) {
   839  	ctx, config, tempdir := initConfigForRunner(t)
   840  	defer os.RemoveAll(tempdir)
   841  	defer libkbfs.CheckConfigAndShutdown(ctx, t, config)
   842  
   843  	git1, err := os.MkdirTemp(os.TempDir(), "kbfsgittest")
   844  	require.NoError(t, err)
   845  	defer os.RemoveAll(git1)
   846  	dotgit1 := filepath.Join(git1, ".git")
   847  
   848  	// Make shared repo with 2 branches.  Make sure there's an initial
   849  	// pack-refs file.
   850  	makeLocalRepoWithOneFile(t, git1, "foo", "hello", "")
   851  	gitExec(t, dotgit1, git1, "pack-refs", "--all")
   852  	testPushWithTemplate(ctx, t, config, git1,
   853  		[]string{"refs/heads/master:refs/heads/master"},
   854  		"ok %s\n\n", "user1,user2")
   855  	testPushWithTemplate(ctx, t, config, git1,
   856  		[]string{"refs/heads/master:refs/heads/test"},
   857  		"ok %s\n\n", "user1,user2")
   858  
   859  	// Config for the second user.
   860  	tempdir2, err := os.MkdirTemp(os.TempDir(), "journal_server")
   861  	require.NoError(t, err)
   862  	defer os.RemoveAll(tempdir2)
   863  	config2 := libkbfs.ConfigAsUser(config, "user2")
   864  	defer libkbfs.CheckConfigAndShutdown(ctx, t, config2)
   865  	err = config2.EnableDiskLimiter(tempdir2)
   866  	require.NoError(t, err)
   867  	err = config2.EnableJournaling(
   868  		ctx, tempdir2, libkbfs.TLFJournalSingleOpBackgroundWorkEnabled)
   869  	require.NoError(t, err)
   870  
   871  	git2, err := os.MkdirTemp(os.TempDir(), "kbfsgittest")
   872  	require.NoError(t, err)
   873  	defer os.RemoveAll(git2)
   874  
   875  	heads := testListAndGetHeadsWithName(ctx, t, config2, git2,
   876  		[]string{"refs/heads/master", "refs/heads/test", "HEAD"}, "user1,user2")
   877  	require.Equal(t, heads[0], heads[1])
   878  
   879  	// Have the second user refpack, but stall it after it takes the lock.
   880  	packOnStalled, packUnstall, packCtx := libkbfs.StallMDOp(
   881  		ctx, config2, libkbfs.StallableMDAfterGetRange, 1)
   882  	packErrCh := make(chan error)
   883  	h, err := tlfhandle.ParseHandle(
   884  		ctx, config2.KBPKI(), config.MDOps(), config, "user1,user2",
   885  		tlf.Private)
   886  	require.NoError(t, err)
   887  	go func() {
   888  		packErrCh <- libgit.GCRepo(
   889  			packCtx, config2, h, "test", libgit.GCOptions{
   890  				MaxLooseRefs:   0,
   891  				MaxObjectPacks: -1,
   892  			})
   893  	}()
   894  	select {
   895  	case <-packOnStalled:
   896  	case <-ctx.Done():
   897  		t.Fatal(ctx.Err())
   898  	}
   899  
   900  	// While the second user is stalled, have the first user delete
   901  	// one of the refs.  Wait until it tries to get the lock, and then
   902  	// release the pack-refs call.
   903  	deleteOnStalled, deleteUnstall, deleteCtx := libkbfs.StallMDOp(
   904  		ctx, config, libkbfs.StallableMDGetRange, 1)
   905  	inputReader, inputWriter := io.Pipe()
   906  	defer inputWriter.Close()
   907  	go func() {
   908  		_, _ = inputWriter.Write([]byte("push :refs/heads/test\n"))
   909  		_, _ = inputWriter.Write([]byte("\n\n"))
   910  	}()
   911  
   912  	var output bytes.Buffer
   913  	deleteRunner, err := newRunner(ctx, config, "origin",
   914  		"keybase://private/user1,user2/test",
   915  		dotgit1, inputReader, &output, testErrput{t})
   916  	require.NoError(t, err)
   917  	deleteErrCh := make(chan error)
   918  	go func() {
   919  		deleteErrCh <- deleteRunner.processCommands(deleteCtx)
   920  	}()
   921  	select {
   922  	case <-deleteOnStalled:
   923  	case <-ctx.Done():
   924  		t.Fatal(ctx.Err())
   925  	}
   926  	// Release it, and it should block on getting the lock.
   927  	close(deleteUnstall)
   928  
   929  	// Now let the pack-refs finish.
   930  	close(packUnstall)
   931  	select {
   932  	case err := <-packErrCh:
   933  		require.NoError(t, err)
   934  	case <-ctx.Done():
   935  		t.Fatal(ctx.Err())
   936  	}
   937  
   938  	// And the delete should finish right after.
   939  	select {
   940  	case err := <-deleteErrCh:
   941  		require.NoError(t, err)
   942  	case <-ctx.Done():
   943  		t.Fatal(ctx.Err())
   944  	}
   945  
   946  	rootNode, _, err := config2.KBFSOps().GetOrCreateRootNode(
   947  		ctx, h, data.MasterBranch)
   948  	require.NoError(t, err)
   949  	err = config2.KBFSOps().SyncFromServer(
   950  		ctx, rootNode.GetFolderBranch(), nil)
   951  	require.NoError(t, err)
   952  	testListAndGetHeadsWithName(ctx, t, config2, git2,
   953  		[]string{"refs/heads/master", "HEAD"}, "user1,user2")
   954  }
   955  
   956  func TestRepackObjects(t *testing.T) {
   957  	ctx, config, tempdir := initConfigForRunner(t)
   958  	defer os.RemoveAll(tempdir)
   959  	defer libkbfs.CheckConfigAndShutdown(ctx, t, config)
   960  
   961  	git, err := os.MkdirTemp(os.TempDir(), "kbfsgittest")
   962  	require.NoError(t, err)
   963  	defer os.RemoveAll(git)
   964  
   965  	h, err := tlfhandle.ParseHandle(
   966  		ctx, config.KBPKI(), config.MDOps(), config, "user1", tlf.Private)
   967  	require.NoError(t, err)
   968  	_, err = libgit.CreateRepoAndID(ctx, config, h, "test")
   969  	require.NoError(t, err)
   970  
   971  	// Make a few pushes to make a few object pack files.
   972  	makeLocalRepoWithOneFile(t, git, "foo", "hello", "")
   973  	testPush(ctx, t, config, git, "refs/heads/master:refs/heads/master")
   974  	addOneFileToRepo(t, git, "foo2", "hello2")
   975  	testPush(ctx, t, config, git, "refs/heads/master:refs/heads/master")
   976  	addOneFileToRepo(t, git, "foo3", "hello3")
   977  	testPush(ctx, t, config, git, "refs/heads/master:refs/heads/master")
   978  	addOneFileToRepo(t, git, "foo4", "hello4")
   979  	testPush(ctx, t, config, git, "refs/heads/master:refs/heads/master")
   980  
   981  	fs, _, err := libgit.GetRepoAndID(ctx, config, h, "test", "")
   982  	require.NoError(t, err)
   983  
   984  	storage, err := libgit.NewGitConfigWithoutRemotesStorer(fs)
   985  	require.NoError(t, err)
   986  	packs, err := storage.ObjectPacks()
   987  	require.NoError(t, err)
   988  	numObjectPacks := len(packs)
   989  	require.Equal(t, 3, numObjectPacks)
   990  
   991  	// Re-pack them all into one.
   992  	err = libgit.GCRepo(
   993  		ctx, config, h, "test", libgit.GCOptions{
   994  			MaxLooseRefs:   100,
   995  			MaxObjectPacks: 0,
   996  		})
   997  	require.NoError(t, err)
   998  
   999  	packs, err = storage.ObjectPacks()
  1000  	require.NoError(t, err)
  1001  	numObjectPacks = len(packs)
  1002  	require.Equal(t, 1, numObjectPacks)
  1003  
  1004  	// Check that a second clone looks correct.
  1005  	git2 := testCloneIntoNewLocalRepo(ctx, t, config, "user1")
  1006  	defer os.RemoveAll(git2)
  1007  
  1008  	checkFile := func(name, expectedData string) {
  1009  		data, err := os.ReadFile(filepath.Join(git2, name))
  1010  		require.NoError(t, err)
  1011  		require.Equal(t, expectedData, string(data))
  1012  	}
  1013  	checkFile("foo", "hello")
  1014  	checkFile("foo2", "hello2")
  1015  	checkFile("foo3", "hello3")
  1016  	checkFile("foo4", "hello4")
  1017  }
  1018  
  1019  func testHandlePushBatch(ctx context.Context, t *testing.T,
  1020  	config libkbfs.Config, git, refspec, tlfName string) libgit.RefDataByName {
  1021  	var input bytes.Buffer
  1022  	var output bytes.Buffer
  1023  	r, err := newRunner(ctx, config, "origin",
  1024  		fmt.Sprintf("keybase://private/%s/test", tlfName),
  1025  		filepath.Join(git, ".git"), &input, &output, testErrput{t})
  1026  	require.NoError(t, err)
  1027  
  1028  	args := [][]string{{refspec}}
  1029  	commits, err := r.handlePushBatch(ctx, args)
  1030  	require.NoError(t, err)
  1031  	return commits
  1032  }
  1033  
  1034  func TestRunnerHandlePushBatch(t *testing.T) {
  1035  	t.Skip("KBFS-3836: currently flaking a lot")
  1036  	ctx, config, tempdir := initConfigForRunner(t)
  1037  	defer os.RemoveAll(tempdir)
  1038  	defer libkbfs.CheckConfigAndShutdown(ctx, t, config)
  1039  
  1040  	git, err := os.MkdirTemp(os.TempDir(), "kbfsgittest")
  1041  	require.NoError(t, err)
  1042  	defer os.RemoveAll(git)
  1043  
  1044  	t.Log("Setup the repository.")
  1045  	h, err := tlfhandle.ParseHandle(
  1046  		ctx, config.KBPKI(), config.MDOps(), config, "user1", tlf.Private)
  1047  	require.NoError(t, err)
  1048  	_, err = libgit.CreateRepoAndID(ctx, config, h, "test")
  1049  	require.NoError(t, err)
  1050  
  1051  	t.Log("Make a new local repo with one commit, and push it. " +
  1052  		"We expect this to return no commits, since it should push the " +
  1053  		"whole repository.")
  1054  	makeLocalRepoWithOneFileCustomCommitMsg(t, git, "foo", "hello", "", "one")
  1055  	refDataByName := testHandlePushBatch(ctx, t, config, git,
  1056  		"refs/heads/master:refs/heads/master", "user1")
  1057  	require.Len(t, refDataByName, 1)
  1058  	master := refDataByName["refs/heads/master"]
  1059  	require.False(t, master.IsDelete)
  1060  	commits := master.Commits
  1061  	require.Len(t, commits, 1)
  1062  	require.Equal(t, "one", strings.TrimSpace(commits[0].Message))
  1063  
  1064  	t.Log("Add a commit and push it. We expect the push batch to return " +
  1065  		"one reference with one commit.")
  1066  	addOneFileToRepoCustomCommitMsg(t, git, "foo2", "hello2", "two")
  1067  	refDataByName = testHandlePushBatch(ctx, t, config, git,
  1068  		"refs/heads/master:refs/heads/master", "user1")
  1069  	require.Len(t, refDataByName, 1)
  1070  	master = refDataByName["refs/heads/master"]
  1071  	require.False(t, master.IsDelete)
  1072  	commits = master.Commits
  1073  	require.Len(t, commits, 1)
  1074  	require.Equal(t, "two", strings.TrimSpace(commits[0].Message))
  1075  
  1076  	t.Log("Add three commits. We expect the push batch to return " +
  1077  		"one reference with three commits. The commits should be ordered " +
  1078  		"with the most recent first.")
  1079  	addOneFileToRepoCustomCommitMsg(t, git, "foo3", "hello3", "three")
  1080  	addOneFileToRepoCustomCommitMsg(t, git, "foo4", "hello4", "four")
  1081  	addOneFileToRepoCustomCommitMsg(t, git, "foo5", "hello5", "five")
  1082  	refDataByName = testHandlePushBatch(ctx, t, config, git,
  1083  		"refs/heads/master:refs/heads/master", "user1")
  1084  	require.Len(t, refDataByName, 1)
  1085  	master = refDataByName["refs/heads/master"]
  1086  	require.False(t, master.IsDelete)
  1087  	commits = master.Commits
  1088  	require.Len(t, commits, 3)
  1089  	require.Equal(t, "five", strings.TrimSpace(commits[0].Message))
  1090  	require.Equal(t, "four", strings.TrimSpace(commits[1].Message))
  1091  	require.Equal(t, "three", strings.TrimSpace(commits[2].Message))
  1092  
  1093  	t.Log("Add more commits than the maximum to visit per ref. " +
  1094  		"Check that a sentinel value was added.")
  1095  	for i := 0; i < maxCommitsToVisitPerRef+1; i++ {
  1096  		filename := fmt.Sprintf("foo%d", i+6)
  1097  		content := fmt.Sprintf("hello%d", i+6)
  1098  		msg := fmt.Sprintf("commit message %d", i+6)
  1099  		addOneFileToRepoCustomCommitMsg(t, git, filename, content, msg)
  1100  	}
  1101  	refDataByName = testHandlePushBatch(ctx, t, config, git,
  1102  		"refs/heads/master:refs/heads/master", "user1")
  1103  	require.Len(t, refDataByName, 1)
  1104  	master = refDataByName["refs/heads/master"]
  1105  	require.False(t, master.IsDelete)
  1106  	commits = master.Commits
  1107  	require.Len(t, commits, maxCommitsToVisitPerRef)
  1108  	require.Equal(t, libgit.CommitSentinelValue, commits[maxCommitsToVisitPerRef-1])
  1109  
  1110  	t.Log("Push a deletion.")
  1111  	refDataByName = testHandlePushBatch(ctx, t, config, git,
  1112  		":refs/heads/master", "user1")
  1113  	require.Len(t, refDataByName, 1)
  1114  	master = refDataByName["refs/heads/master"]
  1115  	require.True(t, master.IsDelete)
  1116  	require.Len(t, master.Commits, 0)
  1117  }
  1118  
  1119  func TestRunnerSubmodule(t *testing.T) {
  1120  	if runtime.GOOS == "windows" {
  1121  		t.Skip("submodule add doesn't work well on Windows")
  1122  	}
  1123  
  1124  	ctx, config, tempdir := initConfigForRunner(t)
  1125  	defer os.RemoveAll(tempdir)
  1126  	defer libkbfs.CheckConfigAndShutdown(ctx, t, config)
  1127  
  1128  	shutdown := libgit.StartAutogit(config, 25)
  1129  	defer shutdown()
  1130  
  1131  	t.Log("Make a local repo that will become a KBFS repo")
  1132  	git1, err := os.MkdirTemp(os.TempDir(), "kbfsgittest")
  1133  	require.NoError(t, err)
  1134  	defer os.RemoveAll(git1)
  1135  	makeLocalRepoWithOneFile(t, git1, "foo", "hello", "")
  1136  	dotgit1 := filepath.Join(git1, ".git")
  1137  
  1138  	t.Log("Make a second local repo that will be a submodule")
  1139  	git2, err := os.MkdirTemp(os.TempDir(), "kbfsgittest2")
  1140  	require.NoError(t, err)
  1141  	defer os.RemoveAll(git2)
  1142  	makeLocalRepoWithOneFile(t, git2, "foo2", "hello2", "")
  1143  	dotgit2 := filepath.Join(git2, ".git")
  1144  
  1145  	t.Log("Add the submodule to the first local repo")
  1146  	// git-submodules requires a real working directory for some reason.
  1147  	err = os.Chdir(git1)
  1148  	require.NoError(t, err)
  1149  	gitExec(t, dotgit1, git1, "submodule", "add", "-f", dotgit2)
  1150  	gitExec(t, dotgit1, git1, "-c", "user.name=Foo",
  1151  		"-c", "user.email=foo@foo.com", "commit", "-a", "-m", "submodule")
  1152  
  1153  	t.Log("Push the first local repo into KBFS")
  1154  	h, err := tlfhandle.ParseHandle(
  1155  		ctx, config.KBPKI(), config.MDOps(), config, "user1", tlf.Private)
  1156  	require.NoError(t, err)
  1157  	_, err = libgit.CreateRepoAndID(ctx, config, h, "test")
  1158  	require.NoError(t, err)
  1159  	testPush(ctx, t, config, git1, "refs/heads/master:refs/heads/master")
  1160  
  1161  	t.Log("Use autogit to browse it")
  1162  	rootFS, err := libfs.NewFS(
  1163  		ctx, config, h, data.MasterBranch, "", "", keybase1.MDPriorityNormal)
  1164  	require.NoError(t, err)
  1165  	fis, err := rootFS.ReadDir(".kbfs_autogit/test")
  1166  	require.NoError(t, err)
  1167  	require.Len(t, fis, 3 /* foo, kbfsgittest2, and .gitmodules */)
  1168  	f, err := rootFS.Open(".kbfs_autogit/test/" + filepath.Base(git2))
  1169  	require.NoError(t, err)
  1170  	defer f.Close()
  1171  	data, err := io.ReadAll(f)
  1172  	require.NoError(t, err)
  1173  	require.True(t, strings.HasPrefix(string(data), "git submodule"))
  1174  }
  1175  
  1176  func TestRunnerLFS(t *testing.T) {
  1177  	ctx, config, tempdir := initConfigForRunner(t)
  1178  	defer libkbfs.CheckConfigAndShutdown(ctx, t, config)
  1179  	defer os.RemoveAll(tempdir)
  1180  
  1181  	localFilePath := filepath.Join(tempdir, "local.txt")
  1182  	f, err := os.Create(localFilePath)
  1183  	require.NoError(t, err)
  1184  	doClose := true
  1185  	defer func() {
  1186  		if doClose {
  1187  			err := f.Close()
  1188  			require.NoError(t, err)
  1189  		}
  1190  	}()
  1191  	lfsData := []byte("hello")
  1192  	_, err = f.Write(lfsData)
  1193  	require.NoError(t, err)
  1194  	err = f.Close()
  1195  	require.NoError(t, err)
  1196  	doClose = false
  1197  
  1198  	inputReader, inputWriter := io.Pipe()
  1199  	defer inputWriter.Close()
  1200  	oid := "bf3e3e2af9366a3b704ae0c31de5afa64193ebabffde2091936ad2e7510bc03a"
  1201  	go func() {
  1202  		_, _ = inputWriter.Write([]byte("{\"event\": \"upload\", \"oid\": \"" + oid + "\", \"size\": 5, \"path\": \"" + filepath.ToSlash(localFilePath) + "\"}\n{\"event\": \"terminate\"}\n"))
  1203  	}()
  1204  
  1205  	h, err := tlfhandle.ParseHandle(
  1206  		ctx, config.KBPKI(), config.MDOps(), config, "user1", tlf.Private)
  1207  	require.NoError(t, err)
  1208  	_, err = libgit.CreateRepoAndID(ctx, config, h, "test")
  1209  	require.NoError(t, err)
  1210  
  1211  	t.Log("Send upload command and make sure it sends the right output")
  1212  	var output bytes.Buffer
  1213  	r, err := newRunnerWithType(
  1214  		ctx, config, "origin", "keybase://private/user1/test", "", inputReader,
  1215  		&output, testErrput{t}, processLFSNoProgress)
  1216  	require.NoError(t, err)
  1217  	err = r.processCommands(ctx)
  1218  	require.NoError(t, err)
  1219  	require.Equal(
  1220  		t, "{\"event\":\"complete\",\"oid\":\""+oid+"\"}\n", output.String())
  1221  
  1222  	t.Log("Make sure the file has been fully uploaded")
  1223  	fs, err := libfs.NewFS(
  1224  		ctx, config, h, data.MasterBranch,
  1225  		fmt.Sprintf("%s/test/%s", kbfsRepoDir, libgit.LFSSubdir), "",
  1226  		keybase1.MDPriorityGit)
  1227  	require.NoError(t, err)
  1228  	oidF, err := fs.Open(oid)
  1229  	require.NoError(t, err)
  1230  	defer oidF.Close()
  1231  	buf, err := io.ReadAll(oidF)
  1232  	require.NoError(t, err)
  1233  	require.Equal(t, lfsData, buf)
  1234  
  1235  	t.Log("Download and check the file")
  1236  	inputReader2, inputWriter2 := io.Pipe()
  1237  	defer inputWriter2.Close()
  1238  	go func() {
  1239  		_, _ = inputWriter2.Write([]byte("{\"event\": \"download\", \"oid\": \"" + oid + "\"}\n{\"event\": \"terminate\"}\n"))
  1240  	}()
  1241  	oldWd, err := os.Getwd()
  1242  	oldWdExists := true
  1243  	if err != nil {
  1244  		if os.IsNotExist(err) {
  1245  			oldWdExists = false
  1246  		} else {
  1247  			require.NoError(t, err)
  1248  		}
  1249  	}
  1250  	err = os.Chdir(tempdir)
  1251  	require.NoError(t, err)
  1252  	if oldWdExists {
  1253  		defer func() {
  1254  			err = os.Chdir(oldWd)
  1255  			require.NoError(t, err)
  1256  		}()
  1257  	}
  1258  	var output2 bytes.Buffer
  1259  	r2, err := newRunnerWithType(
  1260  		ctx, config, "origin", "keybase://private/user1/test", "", inputReader2,
  1261  		&output2, testErrput{t}, processLFSNoProgress)
  1262  	require.NoError(t, err)
  1263  	err = r2.processCommands(ctx)
  1264  	require.NoError(t, err)
  1265  	outbuf := output2.Bytes()
  1266  	var resp lfsResponse
  1267  	err = json.Unmarshal(outbuf, &resp)
  1268  	require.NoError(t, err)
  1269  	p := resp.Path
  1270  	require.Equal(
  1271  		t, "{\"event\":\"complete\",\"oid\":\""+oid+"\",\"path\":\""+p+"\"}\n",
  1272  		output2.String())
  1273  
  1274  	pF, err := os.Open(p)
  1275  	require.NoError(t, err)
  1276  	defer pF.Close()
  1277  	buf, err = io.ReadAll(pF)
  1278  	require.NoError(t, err)
  1279  	require.Equal(t, lfsData, buf)
  1280  }