github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libgit/autogit_node_wrappers.go (about)

     1  // Copyright 2018 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 libgit
     6  
     7  import (
     8  	"context"
     9  	"os"
    10  	"path"
    11  	"strings"
    12  	"sync"
    13  
    14  	"github.com/keybase/client/go/kbfs/data"
    15  	"github.com/keybase/client/go/kbfs/libfs"
    16  	"github.com/keybase/client/go/kbfs/libkbfs"
    17  	"github.com/keybase/client/go/protocol/keybase1"
    18  	billy "gopkg.in/src-d/go-billy.v4"
    19  	"gopkg.in/src-d/go-git.v4/plumbing"
    20  )
    21  
    22  // This file contains libkbfs.Node wrappers for implementing the
    23  // .kbfs_autogit directory structure. It breaks down like this:
    24  //
    25  // * `rootWrapper.wrap()` is installed as a root node wrapper, and wraps
    26  //   the root node for each TLF in a `rootNode` instance.
    27  // * `rootNode` allows .kbfs_autogit to be auto-created when it is
    28  //   looked up, and wraps it two ways, as both a `libkbfs.ReadonlyNode`, and
    29  //   an `autogitRootNode`.
    30  // * `autogitRootNode` lists all the git repos associated with this
    31  //   folder-branch.  It wraps child nodes two ways, as both a
    32  //   `libkbfs.ReadonlyNode` (inherited from `rootNode`), and an
    33  //   `repoDirNode`.
    34  // * `repoDirNode` returns a `*Browser` object when `GetFS()` is
    35  //   called, which is configured to access the corresponding
    36  //   subdirectory within the git repository.  It wraps all children as
    37  //   a `libkbfs.ReadonlyNode` (inherited from `rootNode`); it also
    38  //   wraps subdirectories in `repoDirNode`, and file entries in
    39  //   `repoFileNode`.
    40  // * `repoFileNode` returns a `*browserFile` object when `GetFile()`
    41  //   is called, which is expected to be closed by the caller.
    42  //
    43  // The `*Browser` objects returned are cached in the AutogitManager
    44  // instance, in an LRU-cache, and are cleared whenever the underlying
    45  // repo is updated.  However, this means that the log debug tags that
    46  // are in use may be from the original request that caused the
    47  // `*Browser` to be cached, rather than from the request that is using
    48  // the cached browser.  If this becomes a problem when trying to debug
    49  // stuff, we can modify the go-git `Tree` code to be able to replace
    50  // the underlying storage layer with one that uses the right context.
    51  
    52  const (
    53  	// AutogitRoot is the subdirectory name within a TLF where autogit
    54  	// can be accessed.
    55  	AutogitRoot = ".kbfs_autogit"
    56  	// AutogitBranchPrefix is a prefix of a subdirectory name
    57  	// containing one element of a git reference name.
    58  	AutogitBranchPrefix = ".kbfs_autogit_branch_"
    59  	// AutogitCommitPrefix is a prefix of a file name
    60  	// containing the full commit hash.
    61  	AutogitCommitPrefix = ".kbfs_autogit_commit_"
    62  	// branchSlash can substitute for slashes in branch names,
    63  	// following `AutogitBranchPrefix`.
    64  	branchSlash = "^"
    65  )
    66  
    67  type repoFileNode struct {
    68  	libkbfs.Node
    69  	am        *AutogitManager
    70  	gitRootFS *libfs.FS
    71  	repo      string
    72  	subdir    string
    73  	branch    plumbing.ReferenceName
    74  	filePath  string
    75  }
    76  
    77  var _ libkbfs.Node = (*repoFileNode)(nil)
    78  
    79  func (rfn repoFileNode) GetFile(ctx context.Context) billy.File {
    80  	ctx = libkbfs.CtxWithRandomIDReplayable(
    81  		ctx, ctxAutogitIDKey, ctxAutogitOpID, rfn.am.log)
    82  	_, b, err := rfn.am.GetBrowserForRepo(
    83  		ctx, rfn.gitRootFS, rfn.repo, rfn.branch, rfn.subdir)
    84  	if err != nil {
    85  		rfn.am.log.CDebugf(ctx, "Error getting browser: %+v", err)
    86  		return nil
    87  	}
    88  
    89  	f, err := b.Open(rfn.filePath)
    90  	if err != nil {
    91  		rfn.am.log.CDebugf(ctx, "Error opening file: %+v", err)
    92  		return nil
    93  	}
    94  	return f
    95  }
    96  
    97  type repoCommitNode struct {
    98  	libkbfs.Node
    99  	am        *AutogitManager
   100  	gitRootFS *libfs.FS
   101  	repo      string
   102  	hash      plumbing.Hash
   103  }
   104  
   105  var _ libkbfs.Node = (*repoCommitNode)(nil)
   106  
   107  func (rcn repoCommitNode) GetFile(ctx context.Context) billy.File {
   108  	ctx = libkbfs.CtxWithRandomIDReplayable(
   109  		ctx, ctxAutogitIDKey, ctxAutogitOpID, rcn.am.log)
   110  	_, b, err := rcn.am.GetBrowserForRepo(ctx, rcn.gitRootFS, rcn.repo, "", "")
   111  	if err != nil {
   112  		rcn.am.log.CDebugf(ctx, "Error getting browser: %+v", err)
   113  		return nil
   114  	}
   115  
   116  	f, err := b.getCommitFile(ctx, rcn.hash)
   117  	if err != nil {
   118  		rcn.am.log.CDebugf(ctx, "Error opening file: %+v", err)
   119  		return nil
   120  	}
   121  	return f
   122  }
   123  
   124  type repoDirNode struct {
   125  	libkbfs.Node
   126  	am        *AutogitManager
   127  	gitRootFS *libfs.FS
   128  	repo      string
   129  	subdir    string
   130  	branch    plumbing.ReferenceName
   131  	once      sync.Once
   132  }
   133  
   134  var _ libkbfs.Node = (*repoDirNode)(nil)
   135  
   136  // ShouldCreateMissedLookup implements the Node interface for
   137  // repoDirNode.
   138  func (rdn *repoDirNode) ShouldCreateMissedLookup(
   139  	ctx context.Context, name data.PathPartString) (
   140  	bool, context.Context, data.EntryType, os.FileInfo, data.PathPartString,
   141  	data.BlockPointer) {
   142  	namePlain := name.Plaintext()
   143  	switch {
   144  	case strings.HasPrefix(namePlain, AutogitBranchPrefix):
   145  		branchName := strings.TrimPrefix(namePlain, AutogitBranchPrefix)
   146  		if len(branchName) == 0 {
   147  			return rdn.Node.ShouldCreateMissedLookup(ctx, name)
   148  		}
   149  		// It's difficult to tell if a given name is a legitimate
   150  		// prefix for a branch name or not, so just accept everything.
   151  		// If it's not legit, trying to read the data will error out.
   152  		return true, ctx, data.FakeDir, nil, data.PathPartString{}, data.ZeroPtr
   153  	case strings.HasPrefix(namePlain, AutogitCommitPrefix):
   154  		commit := strings.TrimPrefix(namePlain, AutogitCommitPrefix)
   155  		if len(commit) == 0 {
   156  			return rdn.Node.ShouldCreateMissedLookup(ctx, name)
   157  		}
   158  
   159  		rcn := &repoCommitNode{
   160  			Node:      nil,
   161  			am:        rdn.am,
   162  			gitRootFS: rdn.gitRootFS,
   163  			repo:      rdn.repo,
   164  			hash:      plumbing.NewHash(commit),
   165  		}
   166  		f := rcn.GetFile(ctx)
   167  		if f == nil {
   168  			rdn.am.log.CDebugf(ctx, "Error getting commit file")
   169  			return rdn.Node.ShouldCreateMissedLookup(ctx, name)
   170  		}
   171  		return true, ctx, data.FakeFile, f.(*diffFile).GetInfo(),
   172  			data.PathPartString{}, data.ZeroPtr
   173  	default:
   174  		return rdn.Node.ShouldCreateMissedLookup(ctx, name)
   175  	}
   176  
   177  }
   178  
   179  func (rdn *repoDirNode) GetFS(ctx context.Context) libkbfs.NodeFSReadOnly {
   180  	ctx = libkbfs.CtxWithRandomIDReplayable(
   181  		ctx, ctxAutogitIDKey, ctxAutogitOpID, rdn.am.log)
   182  	_, b, err := rdn.am.GetBrowserForRepo(
   183  		ctx, rdn.gitRootFS, rdn.repo, rdn.branch, rdn.subdir)
   184  	if err != nil {
   185  		rdn.am.log.CDebugf(ctx, "Error getting browser: %+v", err)
   186  		return nil
   187  	}
   188  
   189  	if rdn.subdir == "" {
   190  		// If this is the root node for the repo, register it exactly once.
   191  		rdn.once.Do(func() {
   192  			// TODO(KBFS-4077): remove this debugging when we find the bug
   193  			// where b.tree seems to be disappearing.
   194  			rdn.am.log.CDebugf(
   195  				ctx, "Got browser %p for repo=%s, branch=%s, subdir=%s, "+
   196  					"with tree %p", b, rdn.repo, rdn.branch, rdn.subdir,
   197  				b.tree)
   198  			billyFS, err := rdn.gitRootFS.Chroot(rdn.repo)
   199  			if err != nil {
   200  				rdn.am.log.CDebugf(ctx, "Error getting repo FS: %+v", err)
   201  				return
   202  			}
   203  			repoFS := billyFS.(*libfs.FS)
   204  			rdn.am.registerRepoNode(repoFS.RootNode(), rdn)
   205  		})
   206  	}
   207  
   208  	return b
   209  }
   210  
   211  func (rdn *repoDirNode) WrapChild(child libkbfs.Node) libkbfs.Node {
   212  	child = rdn.Node.WrapChild(child)
   213  	name := child.GetBasename().Plaintext()
   214  
   215  	if rdn.subdir == "" && strings.HasPrefix(name, AutogitBranchPrefix) &&
   216  		rdn.gitRootFS != nil {
   217  		newBranchPart := strings.TrimPrefix(name, AutogitBranchPrefix)
   218  		branch := plumbing.ReferenceName(path.Join(
   219  			string(rdn.branch),
   220  			strings.ReplaceAll(newBranchPart, branchSlash, "/")))
   221  
   222  		return &repoDirNode{
   223  			Node:      child,
   224  			am:        rdn.am,
   225  			gitRootFS: rdn.gitRootFS,
   226  			repo:      rdn.repo,
   227  			subdir:    "",
   228  			branch:    branch,
   229  		}
   230  	} else if strings.HasPrefix(name, AutogitCommitPrefix) {
   231  		commit := strings.TrimPrefix(name, AutogitCommitPrefix)
   232  		return &repoCommitNode{
   233  			Node:      child,
   234  			am:        rdn.am,
   235  			gitRootFS: rdn.gitRootFS,
   236  			repo:      rdn.repo,
   237  			hash:      plumbing.NewHash(commit),
   238  		}
   239  	}
   240  
   241  	if child.EntryType() == data.Dir {
   242  		return &repoDirNode{
   243  			Node:      child,
   244  			am:        rdn.am,
   245  			gitRootFS: rdn.gitRootFS,
   246  			repo:      rdn.repo,
   247  			subdir:    path.Join(rdn.subdir, name),
   248  			branch:    rdn.branch,
   249  		}
   250  	}
   251  	return &repoFileNode{
   252  		Node:      child,
   253  		am:        rdn.am,
   254  		gitRootFS: rdn.gitRootFS,
   255  		repo:      rdn.repo,
   256  		subdir:    rdn.subdir,
   257  		branch:    rdn.branch,
   258  		filePath:  name,
   259  	}
   260  }
   261  
   262  type wrappedRepoList struct {
   263  	*libfs.FS
   264  }
   265  
   266  func (wrl *wrappedRepoList) Stat(repoName string) (os.FileInfo, error) {
   267  	return wrl.FS.Stat(normalizeRepoName(repoName))
   268  }
   269  
   270  func (wrl *wrappedRepoList) Lstat(repoName string) (os.FileInfo, error) {
   271  	return wrl.FS.Lstat(normalizeRepoName(repoName))
   272  }
   273  
   274  // autogitRootNode represents the .kbfs_autogit folder, and lists all
   275  // the git repos associated with the member Node's TLF.
   276  type autogitRootNode struct {
   277  	libkbfs.Node
   278  	am *AutogitManager
   279  	fs *libfs.FS
   280  }
   281  
   282  var _ libkbfs.Node = (*autogitRootNode)(nil)
   283  
   284  func (arn autogitRootNode) GetFS(ctx context.Context) libkbfs.NodeFSReadOnly {
   285  	ctx = libkbfs.CtxWithRandomIDReplayable(
   286  		ctx, ctxAutogitIDKey, ctxAutogitOpID, arn.am.log)
   287  	return &wrappedRepoList{arn.fs.WithContext(ctx)}
   288  }
   289  
   290  // WrapChild implements the Node interface for autogitRootNode.
   291  func (arn autogitRootNode) WrapChild(child libkbfs.Node) libkbfs.Node {
   292  	child = arn.Node.WrapChild(child)
   293  	repo := normalizeRepoName(child.GetBasename().Plaintext())
   294  	return &repoDirNode{
   295  		Node:      child,
   296  		am:        arn.am,
   297  		gitRootFS: arn.fs,
   298  		repo:      repo,
   299  		subdir:    "",
   300  		branch:    "",
   301  	}
   302  }
   303  
   304  // rootNode is a Node wrapper around a TLF root node, that causes the
   305  // autogit root to be created when it is accessed.
   306  type rootNode struct {
   307  	libkbfs.Node
   308  	am *AutogitManager
   309  
   310  	lock sync.RWMutex
   311  	fs   *libfs.FS
   312  }
   313  
   314  var _ libkbfs.Node = (*rootNode)(nil)
   315  
   316  // ShouldCreateMissedLookup implements the Node interface for
   317  // rootNode.
   318  func (rn *rootNode) ShouldCreateMissedLookup(
   319  	ctx context.Context, name data.PathPartString) (
   320  	bool, context.Context, data.EntryType, os.FileInfo, data.PathPartString,
   321  	data.BlockPointer) {
   322  	if name.Plaintext() != AutogitRoot {
   323  		return rn.Node.ShouldCreateMissedLookup(ctx, name)
   324  	}
   325  
   326  	rn.lock.Lock()
   327  	defer rn.lock.Unlock()
   328  	if rn.fs == nil {
   329  		// Make the FS once, in a place where we know the NodeCache
   330  		// won't be locked (to avoid deadlock).
   331  
   332  		h, err := rn.am.config.KBFSOps().GetTLFHandle(ctx, rn)
   333  		if err != nil {
   334  			rn.am.log.CDebugf(ctx, "Error getting handle: %+v", err)
   335  			return rn.Node.ShouldCreateMissedLookup(ctx, name)
   336  		}
   337  
   338  		// Wrap this child so that it will show all the repos.
   339  		ctx := libkbfs.CtxWithRandomIDReplayable(
   340  			context.Background(), ctxAutogitIDKey, ctxAutogitOpID, rn.am.log)
   341  		fs, err := libfs.NewReadonlyFS(
   342  			ctx, rn.am.config, h, rn.GetFolderBranch().Branch, kbfsRepoDir, "",
   343  			keybase1.MDPriorityNormal)
   344  		if err != nil {
   345  			rn.am.log.CDebugf(ctx, "Error making repo FS: %+v", err)
   346  			return rn.Node.ShouldCreateMissedLookup(ctx, name)
   347  		}
   348  		rn.fs = fs
   349  	}
   350  	return true, ctx, data.FakeDir, nil, data.PathPartString{}, data.ZeroPtr
   351  }
   352  
   353  // WrapChild implements the Node interface for rootNode.
   354  func (rn *rootNode) WrapChild(child libkbfs.Node) libkbfs.Node {
   355  	child = rn.Node.WrapChild(child)
   356  	if child.GetBasename().Plaintext() != AutogitRoot {
   357  		return child
   358  	}
   359  
   360  	rn.lock.RLock()
   361  	defer rn.lock.RUnlock()
   362  	if rn.fs == nil {
   363  		rn.am.log.CDebugf(context.TODO(), "FS not available on WrapChild")
   364  		return child
   365  	}
   366  
   367  	rn.am.log.CDebugf(context.TODO(), "Making autogit root node")
   368  	return &autogitRootNode{
   369  		Node: &libkbfs.ReadonlyNode{Node: child},
   370  		am:   rn.am,
   371  		fs:   rn.fs,
   372  	}
   373  }
   374  
   375  // rootWrapper is a struct that manages wrapping root nodes with
   376  // autogit-related context.
   377  type rootWrapper struct {
   378  	am *AutogitManager
   379  }
   380  
   381  func (rw rootWrapper) wrap(node libkbfs.Node) libkbfs.Node {
   382  	return &rootNode{
   383  		Node: node,
   384  		am:   rw.am,
   385  	}
   386  }