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 }