/
githubmirror
/
client
Обзор
Документация
Войти
/
githubmirror
/
client
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
go/git/get.go
377 строк
13 KB
zoom-ua
git tab opts (#29327)
18 июн 2026, 17:23
Не верифицирован
18 июн 2026, 17:23
2c0a052
Код
Авторство
О чём код?
package git import ( "context" "encoding/base64" "fmt" "sort" "sync" "time" "github.com/keybase/client/go/libkb" "github.com/keybase/client/go/protocol/keybase1" "github.com/keybase/client/go/teams" "github.com/keybase/go-codec/codec" "golang.org/x/sync/errgroup" ) type ServerResponseRepo struct { TeamID keybase1.TeamID `json:"team_id"` RepoID keybase1.RepoID `json:"repo_id"` CTime time.Time `json:"ctime"` MTime time.Time `json:"mtime"` EncryptedMetadata string `json:"encrypted_metadata"` EncryptionVersion int `json:"encryption_version"` Nonce string `json:"nonce"` KeyGeneration keybase1.PerTeamKeyGeneration `json:"key_generation"` LastModifyingUID keybase1.UID `json:"last_writer_uid"` LastModifyingDeviceID keybase1.DeviceID `json:"last_writer_device_id"` ChatConvID string `json:"chat_conv_id"` ChatDisabled bool `json:"chat_disabled"` IsImplicit bool `json:"is_implicit"` Role keybase1.TeamRole `json:"role"` } type ServerResponse struct { Repos []ServerResponseRepo `json:"repos"` Status libkb.AppStatus `json:"status"` } type ByRepoMtime []keybase1.GitRepoResult func (c ByRepoMtime) Len() int { return len(c) } func (c ByRepoMtime) Swap(i, j int) { c[i], c[j] = c[j], c[i] } func (c ByRepoMtime) Less(i, j int) bool { res1, err1 := c[i].GetIfOk() res2, err2 := c[j].GetIfOk() if err1 != nil || err2 != nil { return false } return res1.ServerMetadata.Mtime < res2.ServerMetadata.Mtime } var _ libkb.APIResponseWrapper = (*ServerResponse)(nil) // For GetDecode. func (r *ServerResponse) GetAppStatus() *libkb.AppStatus { return &r.Status } func formatRepoURL(folder keybase1.FolderHandle, repoName string) string { return "keybase://" + folder.ToString() + "/" + repoName } // The GUI needs a way to refer to repos func formatUniqueRepoID(teamID keybase1.TeamID, repoID keybase1.RepoID) string { return string(teamID) + "_" + string(repoID) } // Implicit teams need to be converted back into the folder that matches their // display name. Regular teams become a regular team folder. func folderFromTeamID(ctx context.Context, g *libkb.GlobalContext, teamID keybase1.TeamID, isImplicit bool) (keybase1.FolderHandle, error) { if isImplicit { return folderFromTeamIDImplicit(ctx, g, teamID) } return folderFromTeamIDNamed(ctx, g, teamID) } func folderFromTeamIDNamed(ctx context.Context, g *libkb.GlobalContext, teamID keybase1.TeamID) (keybase1.FolderHandle, error) { // Get the name from the FTL rather than teams.ResolveIDToName, which adds a // server resolver round-trip for an untrusted candidate name that it then // verifies via the FTL anyway. The FTL load here already cryptographically // verifies the name itself, and it's cached (Unbox loads the same team), so // this avoids a redundant per-repo round-trip when listing many repos. mctx := libkb.NewMetaContext(ctx, g) res, err := g.GetFastTeamLoader().Load(mctx, keybase1.FastTeamLoadArg{ ID: teamID, Public: teamID.IsPublic(), }) if err != nil { return keybase1.FolderHandle{}, err } return keybase1.FolderHandle{ Name: res.Name.String(), FolderType: keybase1.FolderType_TEAM, }, nil } // folderFromTeamIDImplicit converts from a teamID for implicit teams func folderFromTeamIDImplicit(ctx context.Context, g *libkb.GlobalContext, teamID keybase1.TeamID) (keybase1.FolderHandle, error) { team, err := teams.Load(ctx, g, keybase1.LoadTeamArg{ ID: teamID, Public: teamID.IsPublic(), }) if err != nil { return keybase1.FolderHandle{}, err } if !team.IsImplicit() { return keybase1.FolderHandle{}, fmt.Errorf("Expected an implicit team, but team load said otherwise (%s)", teamID) } // TODO: This function doesn't currently support conflict info. name, err := team.ImplicitTeamDisplayNameString(ctx) if err != nil { return keybase1.FolderHandle{}, err } var folderType keybase1.FolderType if team.IsPublic() { folderType = keybase1.FolderType_PUBLIC } else { folderType = keybase1.FolderType_PRIVATE } return keybase1.FolderHandle{ Name: name, FolderType: folderType, }, nil } // If folder is nil, get for all folders. func getMetadataInner(ctx context.Context, g *libkb.GlobalContext, folder *keybase1.FolderHandle) ([]keybase1.GitRepoResult, error) { mctx := libkb.NewMetaContext(ctx, g) teamer := NewTeamer(g) apiArg := libkb.APIArg{ Endpoint: "kbfs/git/team/get", SessionType: libkb.APISessionTypeREQUIRED, Args: libkb.HTTPArgs{}, // a limit parameter exists, default 100, and we don't currently set it } // The team_id parameter is optional. Add it in if the caller supplied it. if folder != nil { teamIDVis, err := teamer.LookupOrCreate(ctx, *folder) if err != nil { return nil, err } apiArg.Args["team_id"] = libkb.S{Val: string(teamIDVis.TeamID)} } var serverResponse ServerResponse err := mctx.G().GetAPI().GetDecode(mctx, apiArg, &serverResponse) if err != nil { return nil, err } // Unbox the repos in parallel repoCh := make(chan ServerResponseRepo) eg, ctx := errgroup.WithContext(ctx) eg.Go(func() error { defer close(repoCh) for _, responseRepo := range serverResponse.Repos { select { case repoCh <- responseRepo: case <-ctx.Done(): return ctx.Err() } } return nil }) // Initializing the results list to non-nil means that we end up seeing // "[]" instead of "null" in the final JSON output on the CLI, which is // preferable. resultList := []keybase1.GitRepoResult{} var resLock sync.Mutex var firstErr error var anySuccess bool numUnboxThreads := 4 for range numUnboxThreads { eg.Go(func() error { for responseRepo := range repoCh { info, skip, err := getMetadataInnerSingle(ctx, g, folder, responseRepo) resLock.Lock() if err != nil { if firstErr == nil { firstErr = err } mctx.Debug("git.getMetadataInner error (team:%v, repo:%v): %v", responseRepo.TeamID, responseRepo.RepoID, err) resultList = append(resultList, keybase1.NewGitRepoResultWithErr(err.Error())) } else if !skip { anySuccess = true resultList = append(resultList, keybase1.NewGitRepoResultWithOk(*info)) } resLock.Unlock() } return nil }) } if err := eg.Wait(); err != nil { return resultList, err } sort.Sort(ByRepoMtime(resultList)) // If there were no repos, return ok // If all repos failed, return the first error (something is probably wrong that's not repo-specific) // If no repos failed, return ok if len(resultList) == 0 { return resultList, nil } if !anySuccess { return resultList, firstErr } return resultList, nil } // if skip is true the other return values are nil func getMetadataInnerSingle(ctx context.Context, g *libkb.GlobalContext, folder *keybase1.FolderHandle, responseRepo ServerResponseRepo, ) (info *keybase1.GitRepoInfo, skip bool, err error) { cryptoer := NewCrypto(g) // If the folder was passed in, use it. Otherwise, load the team to // figure it out. var repoFolder keybase1.FolderHandle if folder != nil { repoFolder = *folder } else { repoFolder, err = folderFromTeamID(ctx, g, responseRepo.TeamID, responseRepo.IsImplicit) if err != nil { return nil, false, err } // Currently we want to pretend that multi-user personal repos // (/keybase/{private,public}/chris,max/...) don't exist. Short circuit here // to keep those out of the results list. if repoFolder.Name != g.Env.GetUsername().String() && (repoFolder.FolderType == keybase1.FolderType_PRIVATE || repoFolder.FolderType == keybase1.FolderType_PUBLIC) { return nil, true, nil } } teamIDVis := keybase1.TeamIDWithVisibility{ TeamID: responseRepo.TeamID, } if repoFolder.FolderType != keybase1.FolderType_PUBLIC { teamIDVis.Visibility = keybase1.TLFVisibility_PRIVATE } else { teamIDVis.Visibility = keybase1.TLFVisibility_PUBLIC } ciphertext, err := base64.StdEncoding.DecodeString(responseRepo.EncryptedMetadata) if err != nil { return nil, false, err } nonceSlice, err := base64.StdEncoding.DecodeString(responseRepo.Nonce) if err != nil { return nil, false, err } if len(nonceSlice) != len(keybase1.BoxNonce{}) { return nil, false, fmt.Errorf("expected a nonce of length %d, found %d", len(keybase1.BoxNonce{}), len(nonceSlice)) } var nonce keybase1.BoxNonce copy(nonce[:], nonceSlice) encryptedMetadata := keybase1.EncryptedGitMetadata{ V: responseRepo.EncryptionVersion, E: ciphertext, N: nonce, Gen: responseRepo.KeyGeneration, } msgpackPlaintext, err := cryptoer.Unbox(ctx, teamIDVis, &encryptedMetadata) if err != nil { return nil, false, fmt.Errorf("repo tid:%v visibility:%s: %v", teamIDVis.TeamID, teamIDVis.Visibility, err) } var localMetadataVersioned keybase1.GitLocalMetadataVersioned mh := codec.MsgpackHandle{WriteExt: true} dec := codec.NewDecoderBytes(msgpackPlaintext, &mh) err = dec.Decode(&localMetadataVersioned) if err != nil { return nil, false, err } // Translate back from GitLocalMetadataVersioned to the decoupled type // that we use for local RPC. version, err := localMetadataVersioned.Version() if err != nil { return nil, false, err } var localMetadata keybase1.GitLocalMetadata switch version { case keybase1.GitLocalMetadataVersion_V1: localMetadata = keybase1.GitLocalMetadata{ RepoName: localMetadataVersioned.V1().RepoName, } default: return nil, false, fmt.Errorf("unrecognized variant of GitLocalMetadataVersioned: %#v", version) } // Load UPAKs to get the last writer username and device name. The UID -> // username and deviceID -> deviceName mappings are immutable, so we first do // a stale-OK load to avoid a per-repo network poll of the merkle tree (the // dominant cost when loading many repos). Only if the device isn't present in // the cached copy (e.g. our cache predates it) do we force a fresh load. loadUPAK := func(staleOK bool) (*keybase1.UserPlusKeysV2AllIncarnations, error) { upak, _, err := g.GetUPAKLoader().LoadV2(libkb.NewLoadUserArgWithContext(ctx, g). WithUID(responseRepo.LastModifyingUID). WithPublicKeyOptional(). WithStaleOK(staleOK)) return upak, err } findDeviceName := func(upak *keybase1.UserPlusKeysV2AllIncarnations) string { for _, upk := range append([]keybase1.UserPlusKeysV2{upak.Current}, upak.PastIncarnations...) { for _, deviceKey := range upk.DeviceKeys { if deviceKey.DeviceID.Eq(responseRepo.LastModifyingDeviceID) { return deviceKey.DeviceDescription } } } return "" } lastWriterUPAK, err := loadUPAK(true) if err != nil { return nil, false, err } deviceName := findDeviceName(lastWriterUPAK) if deviceName == "" { // The stale copy might be missing the device; force a fresh load before // giving up. lastWriterUPAK, err = loadUPAK(false) if err != nil { return nil, false, err } deviceName = findDeviceName(lastWriterUPAK) } if deviceName == "" { return nil, false, fmt.Errorf("can't find device name for %s's device ID %s", lastWriterUPAK.Current.Username, responseRepo.LastModifyingDeviceID) } var settings *keybase1.GitTeamRepoSettings if repoFolder.FolderType == keybase1.FolderType_TEAM { // ChatDisabled is free (it's in the bulk response). ChannelName needs a // per-repo chat topic-name lookup (the biggest cost of this call), so // the GUI fetches it lazily via GetTeamRepoSettings on row expand. settings = &keybase1.GitTeamRepoSettings{ ChatDisabled: responseRepo.ChatDisabled, } } return &keybase1.GitRepoInfo{ Folder: repoFolder, RepoID: responseRepo.RepoID, RepoUrl: formatRepoURL(repoFolder, string(localMetadata.RepoName)), GlobalUniqueID: formatUniqueRepoID(responseRepo.TeamID, responseRepo.RepoID), CanDelete: responseRepo.Role.IsAdminOrAbove(), LocalMetadata: localMetadata, ServerMetadata: keybase1.GitServerMetadata{ Ctime: keybase1.ToTime(responseRepo.CTime), Mtime: keybase1.ToTime(responseRepo.MTime), LastModifyingUsername: lastWriterUPAK.Current.Username, LastModifyingDeviceID: responseRepo.LastModifyingDeviceID, LastModifyingDeviceName: deviceName, }, TeamRepoSettings: settings, }, false, nil } func GetMetadata(ctx context.Context, g *libkb.GlobalContext, folder keybase1.FolderHandle) ([]keybase1.GitRepoResult, error) { return getMetadataInner(ctx, g, &folder) } func GetAllMetadata(ctx context.Context, g *libkb.GlobalContext) ([]keybase1.GitRepoResult, error) { return getMetadataInner(ctx, g, nil) }