From b7683d4f24c33c602deb258007063c4a14c882b0 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Sun, 5 May 2024 16:09:03 -0400 Subject: [PATCH 01/33] refactor to decouple fs package from gitlab package --- fs/group.go | 70 ++++++++++---------- fs/groups.go | 47 -------------- fs/refresh.go | 14 ++-- fs/repository.go | 24 ++++--- fs/root.go | 74 +++++++++------------ fs/users.go | 159 ---------------------------------------------- gitlab/client.go | 96 +++++++++++++++++++++++----- gitlab/group.go | 157 +++++++++++++++++++++++---------------------- gitlab/project.go | 12 ++++ gitlab/user.go | 124 +++++++++++++++++++----------------- go.mod | 26 ++++++-- go.sum | 3 - main.go | 33 ++++------ 13 files changed, 357 insertions(+), 482 deletions(-) delete mode 100644 fs/groups.go delete mode 100644 fs/users.go diff --git a/fs/group.go b/fs/group.go index 0485e5f..9c0b59e 100644 --- a/fs/group.go +++ b/fs/group.go @@ -2,9 +2,9 @@ package fs import ( "context" + "fmt" "syscall" - "github.com/badjware/gitlabfs/gitlab" "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" ) @@ -13,56 +13,50 @@ type groupNode struct { fs.Inode param *FSParam - group *gitlab.Group + source GroupSource staticNodes map[string]staticNode } +type GroupSource interface { + GetGroupID() uint64 + InvalidateCache() +} + // Ensure we are implementing the NodeReaddirer interface var _ = (fs.NodeReaddirer)((*groupNode)(nil)) // Ensure we are implementing the NodeLookuper interface var _ = (fs.NodeLookuper)((*groupNode)(nil)) -func newGroupNodeByID(gid int, param *FSParam) (*groupNode, error) { - group, err := param.Gitlab.FetchGroup(gid) - if err != nil { - return nil, err - } +func newGroupNodeFromSource(source GroupSource, param *FSParam) (*groupNode, error) { node := &groupNode{ - param: param, - group: group, + param: param, + source: source, staticNodes: map[string]staticNode{ - ".refresh": newRefreshNode(group, param), - }, - } - return node, nil -} - -func newGroupNode(group *gitlab.Group, param *FSParam) (*groupNode, error) { - node := &groupNode{ - param: param, - group: group, - staticNodes: map[string]staticNode{ - ".refresh": newRefreshNode(group, param), + ".refresh": newRefreshNode(source, param), }, } return node, nil } func (n *groupNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { - groupContent, _ := n.param.Gitlab.FetchGroupContent(n.group) - entries := make([]fuse.DirEntry, 0, len(groupContent.Groups)+len(groupContent.Projects)+len(n.staticNodes)) - for _, group := range groupContent.Groups { + groups, repositories, err := n.param.GitPlatform.FetchGroupContent(n.source.GetGroupID()) + if err != nil { + fmt.Errorf("%v", err) + } + + entries := make([]fuse.DirEntry, 0, len(groups)+len(repositories)+len(n.staticNodes)) + for groupName, group := range groups { entries = append(entries, fuse.DirEntry{ - Name: group.Name, - Ino: uint64(group.ID), + Name: groupName, + Ino: group.GetGroupID(), Mode: fuse.S_IFDIR, }) } - for _, project := range groupContent.Projects { + for repositoryName, repository := range repositories { entries = append(entries, fuse.DirEntry{ - Name: project.Name, - Ino: uint64(project.ID), + Name: repositoryName, + Ino: repository.GetRepositoryID(), Mode: fuse.S_IFLNK, }) } @@ -77,27 +71,27 @@ func (n *groupNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { } func (n *groupNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { - groupContent, _ := n.param.Gitlab.FetchGroupContent(n.group) + groups, repositories, _ := n.param.GitPlatform.FetchGroupContent(n.source.GetGroupID()) // Check if the map of groups contains it - group, ok := groupContent.Groups[name] - if ok { + group, found := groups[name] + if found { attrs := fs.StableAttr{ - Ino: uint64(group.ID), + Ino: group.GetGroupID(), Mode: fuse.S_IFDIR, } - groupNode, _ := newGroupNode(group, n.param) + groupNode, _ := newGroupNodeFromSource(group, n.param) return n.NewInode(ctx, groupNode, attrs), 0 } // Check if the map of projects contains it - project, ok := groupContent.Projects[name] - if ok { + repository, found := repositories[name] + if found { attrs := fs.StableAttr{ - Ino: uint64(project.ID), + Ino: repository.GetRepositoryID(), Mode: fuse.S_IFLNK, } - repositoryNode, _ := newRepositoryNode(project, n.param) + repositoryNode, _ := newRepositoryNodeFromSource(repository, n.param) return n.NewInode(ctx, repositoryNode, attrs), 0 } diff --git a/fs/groups.go b/fs/groups.go deleted file mode 100644 index 2635407..0000000 --- a/fs/groups.go +++ /dev/null @@ -1,47 +0,0 @@ -package fs - -import ( - "context" - "fmt" - - "github.com/hanwen/go-fuse/v2/fs" - "github.com/hanwen/go-fuse/v2/fuse" -) - -type groupsNode struct { - fs.Inode - param *FSParam - - rootGroupIds []int -} - -// Ensure we are implementing the NodeOnAdder interface -var _ = (fs.NodeOnAdder)((*groupsNode)(nil)) - -func newGroupsNode(rootGroupIds []int, param *FSParam) *groupsNode { - return &groupsNode{ - param: param, - rootGroupIds: rootGroupIds, - } -} - -func (n *groupsNode) OnAdd(ctx context.Context) { - for _, groupID := range n.rootGroupIds { - groupNode, err := newGroupNodeByID(groupID, n.param) - if err != nil { - fmt.Printf("root group fetch fail: %v\n", err) - fmt.Printf("Please verify the group exists, is public or a token with sufficient permissions is set in the config files.\n") - fmt.Printf("Skipping group %v\n", groupID) - return - } - inode := n.NewPersistentInode( - ctx, - groupNode, - fs.StableAttr{ - Ino: <-n.param.staticInoChan, - Mode: fuse.S_IFDIR, - }, - ) - n.AddChild(groupNode.group.Name, inode, false) - } -} diff --git a/fs/refresh.go b/fs/refresh.go index 30f4974..02bdae6 100644 --- a/fs/refresh.go +++ b/fs/refresh.go @@ -4,15 +4,15 @@ import ( "context" "syscall" - "github.com/badjware/gitlabfs/gitlab" "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" ) type refreshNode struct { fs.Inode - ino uint64 - refresher gitlab.Refresher + ino uint64 + + source GroupSource } // Ensure we are implementing the NodeSetattrer interface @@ -21,10 +21,10 @@ var _ = (fs.NodeSetattrer)((*refreshNode)(nil)) // Ensure we are implementing the NodeOpener interface var _ = (fs.NodeOpener)((*refreshNode)(nil)) -func newRefreshNode(refresher gitlab.Refresher, param *FSParam) *refreshNode { +func newRefreshNode(source GroupSource, param *FSParam) *refreshNode { return &refreshNode{ - ino: <-param.staticInoChan, - refresher: refresher, + ino: <-param.staticInoChan, + source: source, } } @@ -41,6 +41,6 @@ func (n *refreshNode) Setattr(ctx context.Context, fh fs.FileHandle, in *fuse.Se } func (n *refreshNode) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { - n.refresher.InvalidateCache() + n.source.InvalidateCache() return nil, 0, 0 } diff --git a/fs/repository.go b/fs/repository.go index 5370473..046183a 100644 --- a/fs/repository.go +++ b/fs/repository.go @@ -4,23 +4,30 @@ import ( "context" "syscall" - "github.com/badjware/gitlabfs/gitlab" "github.com/hanwen/go-fuse/v2/fs" ) type RepositoryNode struct { fs.Inode - param *FSParam - project *gitlab.Project + param *FSParam + + source RepositorySource +} + +type RepositorySource interface { + // GetName() string + GetRepositoryID() uint64 + GetCloneURL() string + GetDefaultBranch() string } // Ensure we are implementing the NodeReaddirer interface var _ = (fs.NodeReadlinker)((*RepositoryNode)(nil)) -func newRepositoryNode(project *gitlab.Project, param *FSParam) (*RepositoryNode, error) { +func newRepositoryNodeFromSource(source RepositorySource, param *FSParam) (*RepositoryNode, error) { node := &RepositoryNode{ - param: param, - project: project, + param: param, + source: source, } // Passthrough the error if there is one, nothing to add here // Errors on clone/pull are non-fatal @@ -29,7 +36,8 @@ func newRepositoryNode(project *gitlab.Project, param *FSParam) (*RepositoryNode func (n *RepositoryNode) Readlink(ctx context.Context) ([]byte, syscall.Errno) { // Create the local copy of the repo - localRepoLoc, _ := n.param.Git.CloneOrPull(n.project.CloneURL, n.project.ID, n.project.DefaultBranch) + // TODO: cleanup + localRepositoryPath, _ := n.param.GitImplementation.CloneOrPull(n.source.GetCloneURL(), int(n.source.GetRepositoryID()), n.source.GetDefaultBranch()) - return []byte(localRepoLoc), 0 + return []byte(localRepositoryPath), 0 } diff --git a/fs/root.go b/fs/root.go index 9bcfc86..2155bee 100644 --- a/fs/root.go +++ b/fs/root.go @@ -8,8 +8,6 @@ import ( "syscall" "github.com/badjware/gitlabfs/git" - "github.com/badjware/gitlabfs/gitlab" - "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" ) @@ -24,55 +22,25 @@ type staticNode interface { Mode() uint32 } -type FSParam struct { - Git git.GitClonerPuller - Gitlab gitlab.GitlabFetcher +type GitPlatform interface { + FetchRootGroupContent() (map[string]GroupSource, error) + FetchGroupContent(gid uint64) (map[string]GroupSource, map[string]RepositorySource, error) +} - RootGroupIds []int - UserIds []int +type FSParam struct { + GitImplementation git.GitClonerPuller + GitPlatform GitPlatform staticInoChan chan uint64 } type rootNode struct { fs.Inode - param *FSParam - rootGroupIds []int - userIds []int + param *FSParam } var _ = (fs.NodeOnAdder)((*rootNode)(nil)) -func (n *rootNode) OnAdd(ctx context.Context) { - groupsInode := n.NewPersistentInode( - ctx, - newGroupsNode( - n.rootGroupIds, - n.param, - ), - fs.StableAttr{ - Ino: <-n.param.staticInoChan, - Mode: fuse.S_IFDIR, - }, - ) - n.AddChild("groups", groupsInode, false) - - usersInode := n.NewPersistentInode( - ctx, - newUsersNode( - n.userIds, - n.param, - ), - fs.StableAttr{ - Ino: <-n.param.staticInoChan, - Mode: fuse.S_IFDIR, - }, - ) - n.AddChild("users", usersInode, false) - - fmt.Println("Mounted and ready to use") -} - func Start(mountpoint string, mountoptions []string, param *FSParam, debug bool) error { fmt.Printf("Mounting in %v\n", mountpoint) @@ -82,9 +50,7 @@ func Start(mountpoint string, mountoptions []string, param *FSParam, debug bool) param.staticInoChan = make(chan uint64) root := &rootNode{ - param: param, - rootGroupIds: param.RootGroupIds, - userIds: param.UserIds, + param: param, } go staticInoGenerator(root.param.staticInoChan) @@ -104,6 +70,28 @@ func Start(mountpoint string, mountoptions []string, param *FSParam, debug bool) return nil } +func (n *rootNode) OnAdd(ctx context.Context) { + rootGroups, err := n.param.GitPlatform.FetchRootGroupContent() + if err != nil { + panic(err) + } + + for groupName, group := range rootGroups { + groupNode, _ := newGroupNodeFromSource(group, n.param) + persistentInode := n.NewPersistentInode( + ctx, + groupNode, + fs.StableAttr{ + Ino: <-n.param.staticInoChan, + Mode: fuse.S_IFDIR, + }, + ) + n.AddChild(groupName, persistentInode, false) + } + + fmt.Println("Mounted and ready to use") +} + func staticInoGenerator(staticInoChan chan<- uint64) { i := staticInodeStart for { diff --git a/fs/users.go b/fs/users.go deleted file mode 100644 index 6094c68..0000000 --- a/fs/users.go +++ /dev/null @@ -1,159 +0,0 @@ -package fs - -import ( - "context" - "fmt" - "syscall" - - "github.com/badjware/gitlabfs/gitlab" - "github.com/hanwen/go-fuse/v2/fs" - "github.com/hanwen/go-fuse/v2/fuse" -) - -type usersNode struct { - fs.Inode - param *FSParam - - userIds []int -} - -// Ensure we are implementing the NodeOnAdder interface -var _ = (fs.NodeOnAdder)((*usersNode)(nil)) - -func newUsersNode(userIds []int, param *FSParam) *usersNode { - return &usersNode{ - param: param, - userIds: userIds, - } -} - -func (n *usersNode) OnAdd(ctx context.Context) { - // Fetch the current logged user - currentUser, err := n.param.Gitlab.FetchCurrentUser() - // Skip if we are anonymous (or the call fails for some reason...) - if err != nil { - fmt.Println(err) - } else { - currentUserNode, _ := newUserNode(currentUser, n.param) - inode := n.NewPersistentInode( - ctx, - currentUserNode, - fs.StableAttr{ - Ino: <-n.param.staticInoChan, - Mode: fuse.S_IFDIR, - }, - ) - n.AddChild(currentUserNode.user.Name, inode, false) - } - - for _, userID := range n.userIds { - if currentUser != nil && currentUser.ID == userID { - // We already added the current user, we can skip it - continue - } - - userNode, err := newUserNodeByID(userID, n.param) - if err != nil { - fmt.Printf("user fetch fail: %v\n", err) - fmt.Printf("Please verify the user exists and token with sufficient permissions is set in the config files.\n") - fmt.Printf("Skipping user %v\n", userID) - return - } - inode := n.NewPersistentInode( - ctx, - userNode, - fs.StableAttr{ - Ino: <-n.param.staticInoChan, - Mode: fuse.S_IFDIR, - }, - ) - n.AddChild(userNode.user.Name, inode, false) - } -} - -type userNode struct { - fs.Inode - param *FSParam - - user *gitlab.User - staticNodes map[string]staticNode -} - -// Ensure we are implementing the NodeReaddirer interface -var _ = (fs.NodeReaddirer)((*userNode)(nil)) - -// Ensure we are implementing the NodeLookuper interface -var _ = (fs.NodeLookuper)((*userNode)(nil)) - -func newUserNodeByID(uid int, param *FSParam) (*userNode, error) { - user, err := param.Gitlab.FetchUser(uid) - if err != nil { - return nil, err - } - node := &userNode{ - param: param, - user: user, - staticNodes: map[string]staticNode{ - ".refresh": newRefreshNode(user, param), - }, - } - return node, nil -} - -func newUserNode(user *gitlab.User, param *FSParam) (*userNode, error) { - node := &userNode{ - param: param, - user: user, - staticNodes: map[string]staticNode{ - ".refresh": newRefreshNode(user, param), - }, - } - return node, nil -} - -func (n *userNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { - userContent, _ := n.param.Gitlab.FetchUserContent(n.user) - entries := make([]fuse.DirEntry, 0, len(userContent.Projects)+len(n.staticNodes)) - for _, project := range userContent.Projects { - entries = append(entries, fuse.DirEntry{ - Name: project.Name, - Ino: uint64(project.ID), - Mode: fuse.S_IFLNK, - }) - } - for name, staticNode := range n.staticNodes { - entries = append(entries, fuse.DirEntry{ - Name: name, - Ino: staticNode.Ino(), - Mode: staticNode.Mode(), - }) - } - return fs.NewListDirStream(entries), 0 -} - -func (n *userNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { - userContent, _ := n.param.Gitlab.FetchUserContent(n.user) - - // Check if the map of projects contains it - project, ok := userContent.Projects[name] - if ok { - attrs := fs.StableAttr{ - Ino: uint64(project.ID), - Mode: fuse.S_IFLNK, - } - repositoryNode, _ := newRepositoryNode(project, n.param) - return n.NewInode(ctx, repositoryNode, attrs), 0 - } - - // Check if the map of static nodes contains it - staticNode, ok := n.staticNodes[name] - if ok { - attrs := fs.StableAttr{ - Ino: staticNode.Ino(), - Mode: staticNode.Mode(), - } - return n.NewInode(ctx, staticNode, attrs), 0 - } - - return nil, syscall.ENOENT -} diff --git a/gitlab/client.go b/gitlab/client.go index 3c73f44..716e76f 100644 --- a/gitlab/client.go +++ b/gitlab/client.go @@ -2,7 +2,9 @@ package gitlab import ( "fmt" + "slices" + "github.com/badjware/gitlabfs/fs" "github.com/xanzy/go-gitlab" ) @@ -11,26 +13,29 @@ const ( PullMethodSSH = "ssh" ) -type GitlabFetcher interface { - GroupFetcher - UserFetcher -} - -type Refresher interface { - InvalidateCache() -} - -type GitlabClientParam struct { - PullMethod string - IncludeCurrentUser bool +type GitlabClientConfig struct { + URL string `yaml:"url,omitempty"` + Token string `yaml:"token,omitempty"` + GroupIDs []int `yaml:"group_ids,omitempty"` + UserIDs []int `yaml:"user_ids,omitempty"` + IncludeCurrentUser bool `yaml:"include_current_user,omitempty"` + PullMethod string `yaml:"pull_method,omitempty"` } type gitlabClient struct { - GitlabClientParam + GitlabClientConfig client *gitlab.Client + + // root group cache + rootGroupCache map[string]fs.GroupSource + currentUserCache *User + + // API response cache + groupCache map[int]*Group + userCache map[int]*User } -func NewClient(gitlabUrl string, gitlabToken string, p GitlabClientParam) (*gitlabClient, error) { +func NewClient(gitlabUrl string, gitlabToken string, p GitlabClientConfig) (*gitlabClient, error) { client, err := gitlab.NewClient( gitlabToken, gitlab.WithBaseURL(gitlabUrl), @@ -40,8 +45,67 @@ func NewClient(gitlabUrl string, gitlabToken string, p GitlabClientParam) (*gitl } gitlabClient := &gitlabClient{ - GitlabClientParam: p, - client: client, + GitlabClientConfig: p, + client: client, + + rootGroupCache: nil, + currentUserCache: nil, + + groupCache: map[int]*Group{}, + userCache: map[int]*User{}, } return gitlabClient, nil } + +func (c *gitlabClient) FetchRootGroupContent() (map[string]fs.GroupSource, error) { + // use cached values if available + if c.rootGroupCache == nil { + rootGroupCache := make(map[string]fs.GroupSource) + + // fetch root groups + for _, gid := range c.GroupIDs { + group, err := c.fetchGroup(gid) + if err != nil { + return nil, err + } + rootGroupCache[group.Name] = group + } + // fetch users + for _, uid := range c.UserIDs { + user, err := c.fetchUser(uid) + if err != nil { + return nil, err + } + rootGroupCache[user.Name] = user + } + // fetch current user if configured + if c.IncludeCurrentUser { + currentUser, err := c.fetchCurrentUser() + if err != nil { + return nil, err + } + rootGroupCache[currentUser.Name] = currentUser + } + + c.rootGroupCache = rootGroupCache + } + return c.rootGroupCache, nil +} + +func (c *gitlabClient) FetchGroupContent(gid uint64) (map[string]fs.GroupSource, map[string]fs.RepositorySource, error) { + if slices.Contains[[]int, int](c.UserIDs, int(gid)) || (c.currentUserCache != nil && c.currentUserCache.ID == int(gid)) { + // gid is a user + user, err := c.fetchUser(int(gid)) + if err != nil { + return nil, nil, err + } + return c.fetchUserContent(user) + } else { + // gid is a group + group, err := c.fetchGroup(int(gid)) + if err != nil { + return nil, nil, err + } + return c.fetchGroupContent(group) + } +} diff --git a/gitlab/group.go b/gitlab/group.go index 86a4c90..90fae26 100644 --- a/gitlab/group.go +++ b/gitlab/group.go @@ -4,111 +4,118 @@ import ( "fmt" "sync" + "github.com/badjware/gitlabfs/fs" "github.com/xanzy/go-gitlab" ) -type GroupFetcher interface { - FetchGroup(gid int) (*Group, error) - FetchGroupContent(group *Group) (*GroupContent, error) -} - -type GroupContent struct { - Groups map[string]*Group - Projects map[string]*Project -} - type Group struct { ID int Name string - mux sync.Mutex - content *GroupContent + mux sync.Mutex + + // group content cache + groupCache map[string]fs.GroupSource + projectCache map[string]fs.RepositorySource } -func NewGroupFromGitlabGroup(group *gitlab.Group) Group { - // https://godoc.org/github.com/xanzy/go-gitlab#Group - return Group{ - ID: group.ID, - Name: group.Path, - } +func (g *Group) GetGroupID() uint64 { + return uint64(g.ID) } func (g *Group) InvalidateCache() { g.mux.Lock() defer g.mux.Unlock() - g.content = nil + g.groupCache = nil + g.projectCache = nil } -func (c *gitlabClient) FetchGroup(gid int) (*Group, error) { +func (c *gitlabClient) fetchGroup(gid int) (*Group, error) { + // start by searching the cache + // TODO: cache invalidation? + group, found := c.groupCache[gid] + if found { + return group, nil + } + + // If not in cache, fetch group infos from API gitlabGroup, _, err := c.client.Groups.GetGroup(gid) if err != nil { return nil, fmt.Errorf("failed to fetch group with id %v: %v", gid, err) } - group := NewGroupFromGitlabGroup(gitlabGroup) - return &group, nil + newGroup := Group{ + ID: gitlabGroup.ID, + Name: gitlabGroup.Path, + + groupCache: nil, + projectCache: nil, + } + + // save in cache + c.groupCache[gid] = &newGroup + + return &newGroup, nil } -func (c *gitlabClient) FetchGroupContent(group *Group) (*GroupContent, error) { +func (c *gitlabClient) fetchGroupContent(group *Group) (map[string]fs.GroupSource, map[string]fs.RepositorySource, error) { group.mux.Lock() defer group.mux.Unlock() // Get cached data if available - if group.content != nil { - return group.content, nil - } + // TODO: cache cache invalidation? + if group.groupCache == nil || group.projectCache == nil { + groupCache := make(map[string]fs.GroupSource) + projectCache := make(map[string]fs.RepositorySource) - content := &GroupContent{ - Groups: map[string]*Group{}, - Projects: map[string]*Project{}, - } + // List subgroups in path + ListGroupsOpt := &gitlab.ListSubgroupsOptions{ + ListOptions: gitlab.ListOptions{ + Page: 1, + PerPage: 100, + }, + AllAvailable: gitlab.Bool(true), + } + for { + gitlabGroups, response, err := c.client.Groups.ListSubgroups(group.ID, ListGroupsOpt) + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch groups in gitlab: %v", err) + } + for _, gitlabGroup := range gitlabGroups { + group, _ := c.fetchGroup(gitlabGroup.ID) + groupCache[group.Name] = group + } + if response.CurrentPage >= response.TotalPages { + break + } + // Get the next page + ListGroupsOpt.Page = response.NextPage + } - // List subgroups in path - ListGroupsOpt := &gitlab.ListSubgroupsOptions{ - ListOptions: gitlab.ListOptions{ - Page: 1, - PerPage: 100, - }, - AllAvailable: gitlab.Bool(true), - } - for { - gitlabGroups, response, err := c.client.Groups.ListSubgroups(group.ID, ListGroupsOpt) - if err != nil { - return nil, fmt.Errorf("failed to fetch groups in gitlab: %v", err) + // List projects in path + listProjectOpt := &gitlab.ListGroupProjectsOptions{ + ListOptions: gitlab.ListOptions{ + Page: 1, + PerPage: 100, + }} + for { + gitlabProjects, response, err := c.client.Groups.ListGroupProjects(group.ID, listProjectOpt) + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch projects in gitlab: %v", err) + } + for _, gitlabProject := range gitlabProjects { + project := c.newProjectFromGitlabProject(gitlabProject) + projectCache[project.Name] = &project + } + if response.CurrentPage >= response.TotalPages { + break + } + // Get the next page + listProjectOpt.Page = response.NextPage } - for _, gitlabGroup := range gitlabGroups { - group := NewGroupFromGitlabGroup(gitlabGroup) - content.Groups[group.Name] = &group - } - if response.CurrentPage >= response.TotalPages { - break - } - // Get the next page - ListGroupsOpt.Page = response.NextPage - } - // List projects in path - listProjectOpt := &gitlab.ListGroupProjectsOptions{ - ListOptions: gitlab.ListOptions{ - Page: 1, - PerPage: 100, - }} - for { - gitlabProjects, response, err := c.client.Groups.ListGroupProjects(group.ID, listProjectOpt) - if err != nil { - return nil, fmt.Errorf("failed to fetch projects in gitlab: %v", err) - } - for _, gitlabProject := range gitlabProjects { - project := c.newProjectFromGitlabProject(gitlabProject) - content.Projects[project.Name] = &project - } - if response.CurrentPage >= response.TotalPages { - break - } - // Get the next page - listProjectOpt.Page = response.NextPage + group.groupCache = groupCache + group.projectCache = projectCache } - - group.content = content - return content, nil + return group.groupCache, group.projectCache, nil } diff --git a/gitlab/project.go b/gitlab/project.go index 6f789f5..8a5416c 100644 --- a/gitlab/project.go +++ b/gitlab/project.go @@ -11,6 +11,18 @@ type Project struct { DefaultBranch string } +func (p *Project) GetRepositoryID() uint64 { + return uint64(p.ID) +} + +func (p *Project) GetCloneURL() string { + return p.CloneURL +} + +func (p *Project) GetDefaultBranch() string { + return p.DefaultBranch +} + func (c *gitlabClient) newProjectFromGitlabProject(project *gitlab.Project) Project { // https://godoc.org/github.com/xanzy/go-gitlab#Project p := Project{ diff --git a/gitlab/user.go b/gitlab/user.go index a54af3a..7b6de4e 100644 --- a/gitlab/user.go +++ b/gitlab/user.go @@ -1,103 +1,109 @@ package gitlab import ( - "errors" "fmt" "sync" + "github.com/badjware/gitlabfs/fs" "github.com/xanzy/go-gitlab" ) -type UserFetcher interface { - FetchUser(uid int) (*User, error) - FetchCurrentUser() (*User, error) - FetchUserContent(user *User) (*UserContent, error) -} - -type UserContent struct { - Projects map[string]*Project -} - type User struct { ID int Name string - mux sync.Mutex - content *UserContent + mux sync.Mutex + + // user content cache + projectCache map[string]fs.RepositorySource } -func NewUserFromGitlabUser(user *gitlab.User) User { - // https://godoc.org/github.com/xanzy/go-gitlab#User - return User{ - ID: user.ID, - Name: user.Username, - } +func (u *User) GetGroupID() uint64 { + return uint64(u.ID) } func (u *User) InvalidateCache() { u.mux.Lock() defer u.mux.Unlock() - u.content = nil + u.projectCache = nil } -func (c *gitlabClient) FetchUser(uid int) (*User, error) { +func (c *gitlabClient) fetchUser(uid int) (*User, error) { + // start by searching the cache + // TODO: cache invalidation? + user, found := c.userCache[uid] + if found { + return user, nil + } + + // If not in cache, fetch group infos from API gitlabUser, _, err := c.client.Users.GetUser(uid) if err != nil { return nil, fmt.Errorf("failed to fetch user with id %v: %v", uid, err) } - user := NewUserFromGitlabUser(gitlabUser) - return &user, nil + newUser := User{ + ID: gitlabUser.ID, + Name: gitlabUser.Username, + + projectCache: nil, + } + + // save in cache + c.userCache[uid] = &newUser + + return &newUser, nil } -func (c *gitlabClient) FetchCurrentUser() (*User, error) { - if c.IncludeCurrentUser { +func (c *gitlabClient) fetchCurrentUser() (*User, error) { + if c.currentUserCache == nil { gitlabUser, _, err := c.client.Users.CurrentUser() if err != nil { return nil, fmt.Errorf("failed to fetch current user: %v", err) } - user := NewUserFromGitlabUser(gitlabUser) - return &user, nil + newUser := User{ + ID: gitlabUser.ID, + Name: gitlabUser.Username, + + projectCache: nil, + } + c.currentUserCache = &newUser } - // no current user to fetch, return nil - return nil, errors.New("current user fetch is disabled") + return c.currentUserCache, nil } -func (c *gitlabClient) FetchUserContent(user *User) (*UserContent, error) { +func (c *gitlabClient) fetchUserContent(user *User) (map[string]fs.GroupSource, map[string]fs.RepositorySource, error) { user.mux.Lock() defer user.mux.Unlock() // Get cached data if available - if user.content != nil { - return user.content, nil - } + // TODO: cache cache invalidation? + if user.projectCache == nil { + projectCache := make(map[string]fs.RepositorySource) - content := &UserContent{ - Projects: map[string]*Project{}, - } + // Fetch the user repositories + listProjectOpt := &gitlab.ListProjectsOptions{ + ListOptions: gitlab.ListOptions{ + Page: 1, + PerPage: 100, + }} + for { + gitlabProjects, response, err := c.client.Projects.ListUserProjects(user.ID, listProjectOpt) + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch projects in gitlab: %v", err) + } + for _, gitlabProject := range gitlabProjects { + project := c.newProjectFromGitlabProject(gitlabProject) + projectCache[project.Name] = &project + } + if response.CurrentPage >= response.TotalPages { + break + } + // Get the next page + listProjectOpt.Page = response.NextPage + } - // Fetch the user repositories - listProjectOpt := &gitlab.ListProjectsOptions{ - ListOptions: gitlab.ListOptions{ - Page: 1, - PerPage: 100, - }} - for { - gitlabProjects, response, err := c.client.Projects.ListUserProjects(user.ID, listProjectOpt) - if err != nil { - return nil, fmt.Errorf("failed to fetch projects in gitlab: %v", err) - } - for _, gitlabProject := range gitlabProjects { - project := c.newProjectFromGitlabProject(gitlabProject) - content.Projects[project.Name] = &project - } - if response.CurrentPage >= response.TotalPages { - break - } - // Get the next page - listProjectOpt.Page = response.NextPage + user.projectCache = projectCache } - - user.content = content - return content, nil + return make(map[string]fs.GroupSource), user.projectCache, nil } diff --git a/go.mod b/go.mod index e61a2d5..100b39d 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,34 @@ module github.com/badjware/gitlabfs -go 1.15 +go 1.21 + +require ( + github.com/hanwen/go-fuse/v2 v2.1.0 + github.com/vmihailenco/taskq/v3 v3.2.9-0.20211122085105-720ffc56ac4d + github.com/xanzy/go-gitlab v0.47.0 + gopkg.in/yaml.v2 v2.4.0 +) require ( github.com/bsm/redislock v0.7.2 // indirect + github.com/capnm/sysinfo v0.0.0-20130621111458-5909a53897f3 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/go-redis/redis/v8 v8.11.4 // indirect + github.com/go-redis/redis_rate/v9 v9.1.2 // indirect + github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/hanwen/go-fuse/v2 v2.1.0 github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.6.8 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/klauspost/compress v1.14.4 // indirect - github.com/vmihailenco/taskq/v3 v3.2.9-0.20211122085105-720ffc56ac4d - github.com/xanzy/go-gitlab v0.47.0 + github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 // indirect + golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect google.golang.org/appengine v1.6.7 // indirect - gopkg.in/yaml.v2 v2.4.0 + google.golang.org/protobuf v1.26.0 // indirect ) diff --git a/go.sum b/go.sum index 51a4346..6dccd6e 100644 --- a/go.sum +++ b/go.sum @@ -131,7 +131,6 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/hanwen/go-fuse v1.0.0 h1:GxS9Zrn6c35/BnfiVsZVWmsG803xwE7eVRDvcf/BEVc= github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= github.com/hanwen/go-fuse/v2 v2.1.0 h1:+32ffteETaLYClUj0a3aHjZ1hOPxxaNEHiZiujuDaek= github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc= @@ -151,11 +150,9 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/iron-io/iron_go3 v0.0.0-20190916120531-a4a7f74b73ac h1:w5wltlINIIqRTqQ64dASrCo0fM7k9nosPbKCZnkL0W0= github.com/iron-io/iron_go3 v0.0.0-20190916120531-a4a7f74b73ac/go.mod h1:gyMTRVO+ZkEy7wQDyD++okPsBN2q127EpuShhHMWG54= -github.com/jeffh/go.bdd v0.0.0-20120717032931-88f798ee0c74 h1:gyfyP8SEIZHs1u2ivTdIbWRtfaKbg5K79d06vnqroJo= github.com/jeffh/go.bdd v0.0.0-20120717032931-88f798ee0c74/go.mod h1:qNa9FlAfO0U/qNkzYBMH1JKYRMzC+sP9IcyV4U18l98= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= diff --git a/main.go b/main.go index cbe3585..8983393 100644 --- a/main.go +++ b/main.go @@ -16,25 +16,17 @@ import ( type ( Config struct { - FS FSConfig `yaml:"fs,omitempty"` - Gitlab GitlabConfig `yaml:"gitlab,omitempty"` - Git GitConfig `yaml:"git,omitempty"` + FS FSConfig `yaml:"fs,omitempty"` + Gitlab gitlab.GitlabClientConfig `yaml:"gitlab,omitempty"` + Git GitConfig `yaml:"git,omitempty"` } FSConfig struct { Mountpoint string `yaml:"mountpoint,omitempty"` MountOptions string `yaml:"mountoptions,omitempty"` } - GitlabConfig struct { - URL string `yaml:"url,omitempty"` - Token string `yaml:"token,omitempty"` - GroupIDs []int `yaml:"group_ids,omitempty"` - UserIDs []int `yaml:"user_ids,omitempty"` - IncludeCurrentUser bool `yaml:"include_current_user,omitempty"` - } GitConfig struct { CloneLocation string `yaml:"clone_location,omitempty"` Remote string `yaml:"remote,omitempty"` - PullMethod string `yaml:"pull_method,omitempty"` OnClone string `yaml:"on_clone,omitempty"` AutoPull bool `yaml:"auto_pull,omitempty"` Depth int `yaml:"depth,omitempty"` @@ -56,17 +48,17 @@ func loadConfig(configPath string) (*Config, error) { Mountpoint: "", MountOptions: "nodev,nosuid", }, - Gitlab: GitlabConfig{ + Gitlab: gitlab.GitlabClientConfig{ URL: "https://gitlab.com", Token: "", GroupIDs: []int{9970}, UserIDs: []int{}, IncludeCurrentUser: true, + PullMethod: "http", }, Git: GitConfig{ CloneLocation: defaultCloneLocation, Remote: "origin", - PullMethod: "http", OnClone: "init", AutoPull: false, Depth: 0, @@ -91,16 +83,13 @@ func loadConfig(configPath string) (*Config, error) { return config, nil } -func makeGitlabConfig(config *Config) (*gitlab.GitlabClientParam, error) { +func makeGitlabConfig(config *Config) (*gitlab.GitlabClientConfig, error) { // parse pull_method - if config.Git.PullMethod != gitlab.PullMethodHTTP && config.Git.PullMethod != gitlab.PullMethodSSH { + if config.Gitlab.PullMethod != gitlab.PullMethodHTTP && config.Gitlab.PullMethod != gitlab.PullMethodSSH { return nil, fmt.Errorf("pull_method must be either \"%v\" or \"%v\"", gitlab.PullMethodHTTP, gitlab.PullMethodSSH) } - return &gitlab.GitlabClientParam{ - PullMethod: config.Git.PullMethod, - IncludeCurrentUser: config.Gitlab.IncludeCurrentUser && config.Gitlab.Token != "", - }, nil + return &config.Gitlab, nil } func makeGitConfig(config *Config) (*git.GitClientParam, error) { @@ -181,18 +170,18 @@ func main() { gitClient, _ := git.NewClient(*gitClientParam) // Create the gitlab client - gitlabClientParam, err := makeGitlabConfig(config) + GitlabClientConfig, err := makeGitlabConfig(config) if err != nil { fmt.Println(err) os.Exit(1) } - gitlabClient, _ := gitlab.NewClient(config.Gitlab.URL, config.Gitlab.Token, *gitlabClientParam) + gitlabClient, _ := gitlab.NewClient(config.Gitlab.URL, config.Gitlab.Token, *GitlabClientConfig) // Start the filesystem err = fs.Start( mountpoint, parsedMountoptions, - &fs.FSParam{Git: gitClient, Gitlab: gitlabClient, RootGroupIds: config.Gitlab.GroupIDs, UserIds: config.Gitlab.UserIDs}, + &fs.FSParam{GitImplementation: gitClient, GitPlatform: gitlabClient}, *debug, ) if err != nil { From 652b7b43a64604a18b0db724e894890fa69d2093 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Sun, 5 May 2024 16:23:07 -0400 Subject: [PATCH 02/33] rename fs package to fstree to avoid collision with go-fuse --- {fs => fstree}/group.go | 2 +- {fs => fstree}/refresh.go | 2 +- {fs => fstree}/repository.go | 12 ++++++------ {fs => fstree}/root.go | 2 +- gitlab/client.go | 10 +++++----- gitlab/group.go | 12 ++++++------ gitlab/user.go | 10 +++++----- main.go | 6 +++--- 8 files changed, 28 insertions(+), 28 deletions(-) rename {fs => fstree}/group.go (99%) rename {fs => fstree}/refresh.go (98%) rename {fs => fstree}/repository.go (78%) rename {fs => fstree}/root.go (99%) diff --git a/fs/group.go b/fstree/group.go similarity index 99% rename from fs/group.go rename to fstree/group.go index 9c0b59e..de0978f 100644 --- a/fs/group.go +++ b/fstree/group.go @@ -1,4 +1,4 @@ -package fs +package fstree import ( "context" diff --git a/fs/refresh.go b/fstree/refresh.go similarity index 98% rename from fs/refresh.go rename to fstree/refresh.go index 02bdae6..71cfdcc 100644 --- a/fs/refresh.go +++ b/fstree/refresh.go @@ -1,4 +1,4 @@ -package fs +package fstree import ( "context" diff --git a/fs/repository.go b/fstree/repository.go similarity index 78% rename from fs/repository.go rename to fstree/repository.go index 046183a..d37fd3d 100644 --- a/fs/repository.go +++ b/fstree/repository.go @@ -1,4 +1,4 @@ -package fs +package fstree import ( "context" @@ -7,7 +7,7 @@ import ( "github.com/hanwen/go-fuse/v2/fs" ) -type RepositoryNode struct { +type repositoryNode struct { fs.Inode param *FSParam @@ -22,10 +22,10 @@ type RepositorySource interface { } // Ensure we are implementing the NodeReaddirer interface -var _ = (fs.NodeReadlinker)((*RepositoryNode)(nil)) +var _ = (fs.NodeReadlinker)((*repositoryNode)(nil)) -func newRepositoryNodeFromSource(source RepositorySource, param *FSParam) (*RepositoryNode, error) { - node := &RepositoryNode{ +func newRepositoryNodeFromSource(source RepositorySource, param *FSParam) (*repositoryNode, error) { + node := &repositoryNode{ param: param, source: source, } @@ -34,7 +34,7 @@ func newRepositoryNodeFromSource(source RepositorySource, param *FSParam) (*Repo return node, nil } -func (n *RepositoryNode) Readlink(ctx context.Context) ([]byte, syscall.Errno) { +func (n *repositoryNode) Readlink(ctx context.Context) ([]byte, syscall.Errno) { // Create the local copy of the repo // TODO: cleanup localRepositoryPath, _ := n.param.GitImplementation.CloneOrPull(n.source.GetCloneURL(), int(n.source.GetRepositoryID()), n.source.GetDefaultBranch()) diff --git a/fs/root.go b/fstree/root.go similarity index 99% rename from fs/root.go rename to fstree/root.go index 2155bee..5159ca9 100644 --- a/fs/root.go +++ b/fstree/root.go @@ -1,4 +1,4 @@ -package fs +package fstree import ( "context" diff --git a/gitlab/client.go b/gitlab/client.go index 716e76f..ee95af3 100644 --- a/gitlab/client.go +++ b/gitlab/client.go @@ -4,7 +4,7 @@ import ( "fmt" "slices" - "github.com/badjware/gitlabfs/fs" + "github.com/badjware/gitlabfs/fstree" "github.com/xanzy/go-gitlab" ) @@ -27,7 +27,7 @@ type gitlabClient struct { client *gitlab.Client // root group cache - rootGroupCache map[string]fs.GroupSource + rootGroupCache map[string]fstree.GroupSource currentUserCache *User // API response cache @@ -57,10 +57,10 @@ func NewClient(gitlabUrl string, gitlabToken string, p GitlabClientConfig) (*git return gitlabClient, nil } -func (c *gitlabClient) FetchRootGroupContent() (map[string]fs.GroupSource, error) { +func (c *gitlabClient) FetchRootGroupContent() (map[string]fstree.GroupSource, error) { // use cached values if available if c.rootGroupCache == nil { - rootGroupCache := make(map[string]fs.GroupSource) + rootGroupCache := make(map[string]fstree.GroupSource) // fetch root groups for _, gid := range c.GroupIDs { @@ -92,7 +92,7 @@ func (c *gitlabClient) FetchRootGroupContent() (map[string]fs.GroupSource, error return c.rootGroupCache, nil } -func (c *gitlabClient) FetchGroupContent(gid uint64) (map[string]fs.GroupSource, map[string]fs.RepositorySource, error) { +func (c *gitlabClient) FetchGroupContent(gid uint64) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) { if slices.Contains[[]int, int](c.UserIDs, int(gid)) || (c.currentUserCache != nil && c.currentUserCache.ID == int(gid)) { // gid is a user user, err := c.fetchUser(int(gid)) diff --git a/gitlab/group.go b/gitlab/group.go index 90fae26..89d3b3d 100644 --- a/gitlab/group.go +++ b/gitlab/group.go @@ -4,7 +4,7 @@ import ( "fmt" "sync" - "github.com/badjware/gitlabfs/fs" + "github.com/badjware/gitlabfs/fstree" "github.com/xanzy/go-gitlab" ) @@ -15,8 +15,8 @@ type Group struct { mux sync.Mutex // group content cache - groupCache map[string]fs.GroupSource - projectCache map[string]fs.RepositorySource + groupCache map[string]fstree.GroupSource + projectCache map[string]fstree.RepositorySource } func (g *Group) GetGroupID() uint64 { @@ -58,15 +58,15 @@ func (c *gitlabClient) fetchGroup(gid int) (*Group, error) { return &newGroup, nil } -func (c *gitlabClient) fetchGroupContent(group *Group) (map[string]fs.GroupSource, map[string]fs.RepositorySource, error) { +func (c *gitlabClient) fetchGroupContent(group *Group) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) { group.mux.Lock() defer group.mux.Unlock() // Get cached data if available // TODO: cache cache invalidation? if group.groupCache == nil || group.projectCache == nil { - groupCache := make(map[string]fs.GroupSource) - projectCache := make(map[string]fs.RepositorySource) + groupCache := make(map[string]fstree.GroupSource) + projectCache := make(map[string]fstree.RepositorySource) // List subgroups in path ListGroupsOpt := &gitlab.ListSubgroupsOptions{ diff --git a/gitlab/user.go b/gitlab/user.go index 7b6de4e..480e35e 100644 --- a/gitlab/user.go +++ b/gitlab/user.go @@ -4,7 +4,7 @@ import ( "fmt" "sync" - "github.com/badjware/gitlabfs/fs" + "github.com/badjware/gitlabfs/fstree" "github.com/xanzy/go-gitlab" ) @@ -15,7 +15,7 @@ type User struct { mux sync.Mutex // user content cache - projectCache map[string]fs.RepositorySource + projectCache map[string]fstree.RepositorySource } func (u *User) GetGroupID() uint64 { @@ -72,14 +72,14 @@ func (c *gitlabClient) fetchCurrentUser() (*User, error) { return c.currentUserCache, nil } -func (c *gitlabClient) fetchUserContent(user *User) (map[string]fs.GroupSource, map[string]fs.RepositorySource, error) { +func (c *gitlabClient) fetchUserContent(user *User) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) { user.mux.Lock() defer user.mux.Unlock() // Get cached data if available // TODO: cache cache invalidation? if user.projectCache == nil { - projectCache := make(map[string]fs.RepositorySource) + projectCache := make(map[string]fstree.RepositorySource) // Fetch the user repositories listProjectOpt := &gitlab.ListProjectsOptions{ @@ -105,5 +105,5 @@ func (c *gitlabClient) fetchUserContent(user *User) (map[string]fs.GroupSource, user.projectCache = projectCache } - return make(map[string]fs.GroupSource), user.projectCache, nil + return make(map[string]fstree.GroupSource), user.projectCache, nil } diff --git a/main.go b/main.go index 8983393..2ef9960 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,7 @@ import ( "path/filepath" "strings" - "github.com/badjware/gitlabfs/fs" + "github.com/badjware/gitlabfs/fstree" "github.com/badjware/gitlabfs/git" "github.com/badjware/gitlabfs/gitlab" "gopkg.in/yaml.v2" @@ -178,10 +178,10 @@ func main() { gitlabClient, _ := gitlab.NewClient(config.Gitlab.URL, config.Gitlab.Token, *GitlabClientConfig) // Start the filesystem - err = fs.Start( + err = fstree.Start( mountpoint, parsedMountoptions, - &fs.FSParam{GitImplementation: gitClient, GitPlatform: gitlabClient}, + &fstree.FSParam{GitImplementation: gitClient, GitPlatform: gitlabClient}, *debug, ) if err != nil { From cf77b16b23fb0c6e193dfed4921b925b332dc713 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Sun, 5 May 2024 19:52:57 -0400 Subject: [PATCH 03/33] refactor to decouple fstree package from git package --- fstree/group.go | 2 +- fstree/repository.go | 7 ++++-- fstree/root.go | 9 ++++--- git/client.go | 56 +++++++++++++++++++++++--------------------- git/clone.go | 12 +++++----- git/pull.go | 6 ++--- main.go | 40 ++++--------------------------- 7 files changed, 55 insertions(+), 77 deletions(-) diff --git a/fstree/group.go b/fstree/group.go index de0978f..76782dd 100644 --- a/fstree/group.go +++ b/fstree/group.go @@ -42,7 +42,7 @@ func newGroupNodeFromSource(source GroupSource, param *FSParam) (*groupNode, err func (n *groupNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { groups, repositories, err := n.param.GitPlatform.FetchGroupContent(n.source.GetGroupID()) if err != nil { - fmt.Errorf("%v", err) + fmt.Println(err) } entries := make([]fuse.DirEntry, 0, len(groups)+len(repositories)+len(n.staticNodes)) diff --git a/fstree/repository.go b/fstree/repository.go index d37fd3d..d8fbbfe 100644 --- a/fstree/repository.go +++ b/fstree/repository.go @@ -2,6 +2,7 @@ package fstree import ( "context" + "fmt" "syscall" "github.com/hanwen/go-fuse/v2/fs" @@ -37,7 +38,9 @@ func newRepositoryNodeFromSource(source RepositorySource, param *FSParam) (*repo func (n *repositoryNode) Readlink(ctx context.Context) ([]byte, syscall.Errno) { // Create the local copy of the repo // TODO: cleanup - localRepositoryPath, _ := n.param.GitImplementation.CloneOrPull(n.source.GetCloneURL(), int(n.source.GetRepositoryID()), n.source.GetDefaultBranch()) - + localRepositoryPath, err := n.param.GitClient.FetchLocalRepositoryPath(n.source) + if err != nil { + fmt.Println(err) + } return []byte(localRepositoryPath), 0 } diff --git a/fstree/root.go b/fstree/root.go index 5159ca9..0ba9b4f 100644 --- a/fstree/root.go +++ b/fstree/root.go @@ -7,7 +7,6 @@ import ( "os/signal" "syscall" - "github.com/badjware/gitlabfs/git" "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" ) @@ -22,14 +21,18 @@ type staticNode interface { Mode() uint32 } +type GitClient interface { + FetchLocalRepositoryPath(source RepositorySource) (string, error) +} + type GitPlatform interface { FetchRootGroupContent() (map[string]GroupSource, error) FetchGroupContent(gid uint64) (map[string]GroupSource, map[string]RepositorySource, error) } type FSParam struct { - GitImplementation git.GitClonerPuller - GitPlatform GitPlatform + GitClient GitClient + GitPlatform GitPlatform staticInoChan chan uint64 } diff --git a/git/client.go b/git/client.go index 868e255..fa73f7d 100644 --- a/git/client.go +++ b/git/client.go @@ -2,39 +2,33 @@ package git import ( "context" - "net/url" + "fmt" "os" "path/filepath" + "regexp" "strconv" "time" + "github.com/badjware/gitlabfs/fstree" "github.com/vmihailenco/taskq/v3" "github.com/vmihailenco/taskq/v3/memqueue" ) -const ( - CloneInit = iota - CloneClone = iota -) - -type GitClonerPuller interface { - CloneOrPull(url string, pid int, defaultBranch string) (localRepoLoc string, err error) -} - type GitClientParam struct { - CloneLocation string - RemoteName string - RemoteURL *url.URL - CloneMethod int - PullDepth int - AutoPull bool - - QueueSize int - QueueWorkerCount int + CloneLocation string `yaml:"clone_location,omitempty"` + Remote string `yaml:"remote,omitempty"` + OnClone string `yaml:"on_clone,omitempty"` + AutoPull bool `yaml:"auto_pull,omitempty"` + Depth int `yaml:"depth,omitempty"` + QueueSize int `yaml:"queue_size,omitempty"` + QueueWorkerCount int `yaml:"worker_count,omitempty"` } type gitClient struct { GitClientParam + + hostnameProg *regexp.Regexp + queue taskq.Queue cloneTask *taskq.Task pullTask *taskq.Task @@ -46,6 +40,8 @@ func NewClient(p GitClientParam) (*gitClient, error) { c := &gitClient{ GitClientParam: p, + hostnameProg: regexp.MustCompile(`([a-z0-1]+\.)+[a-z0-1]+`), + queue: queueFactory.RegisterQueue(&taskq.QueueOptions{ Name: "git-queue", MaxNumWorker: int32(p.QueueWorkerCount), @@ -68,21 +64,27 @@ func NewClient(p GitClientParam) (*gitClient, error) { return c, nil } -func (c *gitClient) getLocalRepoLoc(pid int) string { - return filepath.Join(c.CloneLocation, c.RemoteURL.Hostname(), strconv.Itoa(pid)) -} +func (c *gitClient) FetchLocalRepositoryPath(source fstree.RepositorySource) (localRepoLoc string, err error) { + rid := source.GetRepositoryID() + cloneUrl := source.GetCloneURL() + defaultBranch := source.GetDefaultBranch() -func (c *gitClient) CloneOrPull(url string, pid int, defaultBranch string) (localRepoLoc string, err error) { - localRepoLoc = c.getLocalRepoLoc(pid) + // Parse the url + hostname := c.hostnameProg.FindString(cloneUrl) + if hostname == "" { + return "", fmt.Errorf("failed to match a valid hostname from \"%v\"", cloneUrl) + } + + localRepoLoc = filepath.Join(c.CloneLocation, hostname, strconv.Itoa(int(rid))) if _, err := os.Stat(localRepoLoc); os.IsNotExist(err) { // Dispatch clone msg - msg := c.cloneTask.WithArgs(context.Background(), url, defaultBranch, localRepoLoc) - msg.OnceInPeriod(time.Second, pid) + msg := c.cloneTask.WithArgs(context.Background(), cloneUrl, defaultBranch, localRepoLoc) + msg.OnceInPeriod(time.Second, rid) c.queue.Add(msg) } else if c.AutoPull { // Dispatch pull msg msg := c.pullTask.WithArgs(context.Background(), localRepoLoc, defaultBranch) - msg.OnceInPeriod(time.Second, pid) + msg.OnceInPeriod(time.Second, rid) c.queue.Add(msg) } return localRepoLoc, nil diff --git a/git/clone.go b/git/clone.go index 8a92920..10b9f3b 100644 --- a/git/clone.go +++ b/git/clone.go @@ -8,7 +8,7 @@ import ( ) func (c *gitClient) clone(url string, defaultBranch string, dst string) error { - if c.CloneMethod == CloneInit { + if c.GitClientParam.OnClone == "init" { // "Fake" cloning the repo by never actually talking to the git server // This skip a fetch operation that we would do if we where to do a proper clone // We can save a lot of time and network i/o doing it this way, at the cost of @@ -32,8 +32,8 @@ func (c *gitClient) clone(url string, defaultBranch string, dst string) error { "git", "remote", "add", "-m", defaultBranch, "--", - c.RemoteName, // name - url, // url + c.GitClientParam.Remote, // name + url, // url ) if err != nil { return fmt.Errorf("failed to setup remote %v in git repo %v: %v", url, dst, err) @@ -45,7 +45,7 @@ func (c *gitClient) clone(url string, defaultBranch string, dst string) error { "git", "config", "--local", "--", fmt.Sprintf("branch.%s.remote", defaultBranch), // key - c.RemoteName, // value + c.GitClientParam.Remote, // value ) if err != nil { @@ -66,8 +66,8 @@ func (c *gitClient) clone(url string, defaultBranch string, dst string) error { // Clone the repo _, err := utils.ExecProcess( "git", "clone", - "--origin", c.RemoteName, - "--depth", strconv.Itoa(c.PullDepth), + "--origin", c.GitClientParam.Remote, + "--depth", strconv.Itoa(c.GitClientParam.Depth), "--", url, // repository dst, // directory diff --git a/git/pull.go b/git/pull.go index 31096a4..b53d908 100644 --- a/git/pull.go +++ b/git/pull.go @@ -23,10 +23,10 @@ func (c *gitClient) pull(repoPath string, defaultBranch string) error { _, err = utils.ExecProcessInDir( repoPath, // workdir "git", "pull", - "--depth", strconv.Itoa(c.PullDepth), + "--depth", strconv.Itoa(c.GitClientParam.Depth), "--", - c.RemoteName, // repository - defaultBranch, // refspec + c.GitClientParam.Remote, // repository + defaultBranch, // refspec ) if err != nil { return fmt.Errorf("failed to pull git repo %v: %v", repoPath, err) diff --git a/main.go b/main.go index 2ef9960..e10355e 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,6 @@ package main import ( "flag" "fmt" - "net/url" "os" "path/filepath" "strings" @@ -18,21 +17,12 @@ type ( Config struct { FS FSConfig `yaml:"fs,omitempty"` Gitlab gitlab.GitlabClientConfig `yaml:"gitlab,omitempty"` - Git GitConfig `yaml:"git,omitempty"` + Git git.GitClientParam `yaml:"git,omitempty"` } FSConfig struct { Mountpoint string `yaml:"mountpoint,omitempty"` MountOptions string `yaml:"mountoptions,omitempty"` } - GitConfig struct { - CloneLocation string `yaml:"clone_location,omitempty"` - Remote string `yaml:"remote,omitempty"` - OnClone string `yaml:"on_clone,omitempty"` - AutoPull bool `yaml:"auto_pull,omitempty"` - Depth int `yaml:"depth,omitempty"` - QueueSize int `yaml:"queue_size,omitempty"` - QueueWorkerCount int `yaml:"worker_count,omitempty"` - } ) func loadConfig(configPath string) (*Config, error) { @@ -56,7 +46,7 @@ func loadConfig(configPath string) (*Config, error) { IncludeCurrentUser: true, PullMethod: "http", }, - Git: GitConfig{ + Git: git.GitClientParam{ CloneLocation: defaultCloneLocation, Remote: "origin", OnClone: "init", @@ -93,32 +83,12 @@ func makeGitlabConfig(config *Config) (*gitlab.GitlabClientConfig, error) { } func makeGitConfig(config *Config) (*git.GitClientParam, error) { - // Parse the gilab url - parsedGitlabURL, err := url.Parse(config.Gitlab.URL) - if err != nil { - return nil, err - } - // parse on_clone - cloneMethod := 0 - if config.Git.OnClone == "init" { - cloneMethod = git.CloneInit - } else if config.Git.OnClone == "clone" { - cloneMethod = git.CloneClone - } else { + if config.Git.OnClone != "init" && config.Git.OnClone != "clone" { return nil, fmt.Errorf("on_clone must be either \"init\" or \"clone\"") } - return &git.GitClientParam{ - CloneLocation: config.Git.CloneLocation, - RemoteName: config.Git.Remote, - RemoteURL: parsedGitlabURL, - CloneMethod: cloneMethod, - AutoPull: config.Git.AutoPull, - PullDepth: config.Git.Depth, - QueueSize: config.Git.QueueSize, - QueueWorkerCount: config.Git.QueueWorkerCount, - }, nil + return &config.Git, nil } func main() { @@ -181,7 +151,7 @@ func main() { err = fstree.Start( mountpoint, parsedMountoptions, - &fstree.FSParam{GitImplementation: gitClient, GitPlatform: gitlabClient}, + &fstree.FSParam{GitClient: gitClient, GitPlatform: gitlabClient}, *debug, ) if err != nil { From de68c4952ea0109ab9ede143e5613d1efeb6b2c4 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Sun, 5 May 2024 21:06:34 -0400 Subject: [PATCH 04/33] reorganize gitlab package --- main.go | 2 +- {gitlab => platforms/gitlab}/client.go | 0 {gitlab => platforms/gitlab}/group.go | 0 {gitlab => platforms/gitlab}/project.go | 0 {gitlab => platforms/gitlab}/user.go | 0 5 files changed, 1 insertion(+), 1 deletion(-) rename {gitlab => platforms/gitlab}/client.go (100%) rename {gitlab => platforms/gitlab}/group.go (100%) rename {gitlab => platforms/gitlab}/project.go (100%) rename {gitlab => platforms/gitlab}/user.go (100%) diff --git a/main.go b/main.go index e10355e..1fd4c84 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,7 @@ import ( "github.com/badjware/gitlabfs/fstree" "github.com/badjware/gitlabfs/git" - "github.com/badjware/gitlabfs/gitlab" + "github.com/badjware/gitlabfs/platforms/gitlab" "gopkg.in/yaml.v2" ) diff --git a/gitlab/client.go b/platforms/gitlab/client.go similarity index 100% rename from gitlab/client.go rename to platforms/gitlab/client.go diff --git a/gitlab/group.go b/platforms/gitlab/group.go similarity index 100% rename from gitlab/group.go rename to platforms/gitlab/group.go diff --git a/gitlab/project.go b/platforms/gitlab/project.go similarity index 100% rename from gitlab/project.go rename to platforms/gitlab/project.go diff --git a/gitlab/user.go b/platforms/gitlab/user.go similarity index 100% rename from gitlab/user.go rename to platforms/gitlab/user.go From 324be5f1f6501f048546e2ea2cc39dd8a3b23f74 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Tue, 7 May 2024 23:31:52 -0400 Subject: [PATCH 05/33] entirely skip passing the --depth argument to git if the depth is configured to 0 fixes #7 --- git/clone.go | 12 +++++++++--- git/pull.go | 13 +++++++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/git/clone.go b/git/clone.go index 10b9f3b..ac7ea3a 100644 --- a/git/clone.go +++ b/git/clone.go @@ -64,14 +64,20 @@ func (c *gitClient) clone(url string, defaultBranch string, dst string) error { } } else { // Clone the repo - _, err := utils.ExecProcess( - "git", "clone", + args := []string{ + "clone", "--origin", c.GitClientParam.Remote, - "--depth", strconv.Itoa(c.GitClientParam.Depth), + } + if c.GitClientParam.Depth != 0 { + args = append(args, "--depth", strconv.Itoa(c.GitClientParam.Depth)) + } + args = append(args, "--", url, // repository dst, // directory ) + + _, err := utils.ExecProcess("git", args...) if err != nil { return fmt.Errorf("failed to clone git repo %v to %v: %v", url, dst, err) } diff --git a/git/pull.go b/git/pull.go index b53d908..549ec69 100644 --- a/git/pull.go +++ b/git/pull.go @@ -20,14 +20,19 @@ func (c *gitClient) pull(repoPath string, defaultBranch string) error { if branchName == defaultBranch { // Pull the repo - _, err = utils.ExecProcessInDir( - repoPath, // workdir - "git", "pull", - "--depth", strconv.Itoa(c.GitClientParam.Depth), + args := []string{ + "pull", + } + if c.GitClientParam.Depth != 0 { + args = append(args, "--depth", strconv.Itoa(c.GitClientParam.Depth)) + } + args = append(args, "--", c.GitClientParam.Remote, // repository defaultBranch, // refspec ) + + _, err = utils.ExecProcessInDir(repoPath, "git", args...) if err != nil { return fmt.Errorf("failed to pull git repo %v: %v", repoPath, err) } From 0ece3f05a3c7f3647d49b8cde4ee75c91a8bf956 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Tue, 7 May 2024 23:58:14 -0400 Subject: [PATCH 06/33] Check if git support --initial-branch before attempting to use it on init `git init --initial-branch` was only added in git version 0.28.0. We parse the git version and check if the argument is supported before using it Fixes #8 --- git/client.go | 24 ++++++++++++++++++++++++ git/clone.go | 13 ++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/git/client.go b/git/client.go index fa73f7d..4ab3b8d 100644 --- a/git/client.go +++ b/git/client.go @@ -10,6 +10,7 @@ import ( "time" "github.com/badjware/gitlabfs/fstree" + "github.com/badjware/gitlabfs/utils" "github.com/vmihailenco/taskq/v3" "github.com/vmihailenco/taskq/v3/memqueue" ) @@ -29,6 +30,10 @@ type gitClient struct { hostnameProg *regexp.Regexp + majorVersion int + minorVersion int + patchVersion string + queue taskq.Queue cloneTask *taskq.Task pullTask *taskq.Task @@ -50,6 +55,25 @@ func NewClient(p GitClientParam) (*gitClient, error) { }), } + // Parse git version + gitVersionOutput, err := utils.ExecProcess("git", "--version") + if err != nil { + return nil, fmt.Errorf("failed to run \"git --version\": %v", err) + } + prog := regexp.MustCompile(`([0-9]+)\.([0-9]+)\.(.+)`) + gitVersionMatches := prog.FindStringSubmatch(gitVersionOutput) + c.majorVersion, err = strconv.Atoi(gitVersionMatches[1]) + if err != nil { + return nil, fmt.Errorf("failed to parse git major version \"%v\": %v", gitVersionOutput, err) + } + c.minorVersion, err = strconv.Atoi(gitVersionMatches[2]) + if err != nil { + return nil, fmt.Errorf("failed to parse git minor version \"%v\": %v", gitVersionOutput, err) + } + c.patchVersion = gitVersionMatches[3] + fmt.Printf("Detected git version: major = %v minor = %v patch = %v\n", c.majorVersion, c.minorVersion, c.patchVersion) + + // Register tasks c.cloneTask = taskq.RegisterTask(&taskq.TaskOptions{ Name: "git-clone", Handler: c.clone, diff --git a/git/clone.go b/git/clone.go index ac7ea3a..4efac57 100644 --- a/git/clone.go +++ b/git/clone.go @@ -16,12 +16,19 @@ func (c *gitClient) clone(url string, defaultBranch string, dst string) error { // Init the local repo fmt.Printf("Initializing %v into %v\n", url, dst) - _, err := utils.ExecProcess( - "git", "init", - "--initial-branch", defaultBranch, + args := []string{ + "init", + } + if c.majorVersion > 2 || c.majorVersion == 2 && c.minorVersion >= 28 { + args = append(args, "--initial-branch", defaultBranch) + } else { + fmt.Printf("Version of git is too old to support --initial-branch. Consider upgrading git to version >= 2.28.0") + } + args = append(args, "--", dst, // directory ) + _, err := utils.ExecProcess("git", args...) if err != nil { return fmt.Errorf("failed to init git repo %v to %v: %v", url, dst, err) } From b0b7f7b36d196530e5ae71823a287a940c2d45f3 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Wed, 8 May 2024 19:34:29 -0400 Subject: [PATCH 07/33] improve hostname match regex --- git/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/client.go b/git/client.go index 4ab3b8d..e1ee3f7 100644 --- a/git/client.go +++ b/git/client.go @@ -45,7 +45,7 @@ func NewClient(p GitClientParam) (*gitClient, error) { c := &gitClient{ GitClientParam: p, - hostnameProg: regexp.MustCompile(`([a-z0-1]+\.)+[a-z0-1]+`), + hostnameProg: regexp.MustCompile(`([a-z0-1:\-]+\.)+[a-z0-1:\-]+`), queue: queueFactory.RegisterQueue(&taskq.QueueOptions{ Name: "git-queue", From 8e350a7dac7e037f7ca9c34620f679213d6401ee Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Thu, 6 Jun 2024 01:51:34 -0400 Subject: [PATCH 08/33] switch to slog for logging --- fstree/group.go | 3 +-- fstree/repository.go | 3 +-- fstree/root.go | 19 +++++++++++-------- git/client.go | 11 ++++++++--- git/clone.go | 12 ++++++++---- git/pull.go | 5 +++-- main.go | 9 +++++++-- platforms/gitlab/client.go | 12 +++++++++--- utils/process.go | 10 +++++----- 9 files changed, 53 insertions(+), 31 deletions(-) diff --git a/fstree/group.go b/fstree/group.go index 76782dd..a820e62 100644 --- a/fstree/group.go +++ b/fstree/group.go @@ -2,7 +2,6 @@ package fstree import ( "context" - "fmt" "syscall" "github.com/hanwen/go-fuse/v2/fs" @@ -42,7 +41,7 @@ func newGroupNodeFromSource(source GroupSource, param *FSParam) (*groupNode, err func (n *groupNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { groups, repositories, err := n.param.GitPlatform.FetchGroupContent(n.source.GetGroupID()) if err != nil { - fmt.Println(err) + n.param.logger.Error(err.Error()) } entries := make([]fuse.DirEntry, 0, len(groups)+len(repositories)+len(n.staticNodes)) diff --git a/fstree/repository.go b/fstree/repository.go index d8fbbfe..29539b0 100644 --- a/fstree/repository.go +++ b/fstree/repository.go @@ -2,7 +2,6 @@ package fstree import ( "context" - "fmt" "syscall" "github.com/hanwen/go-fuse/v2/fs" @@ -40,7 +39,7 @@ func (n *repositoryNode) Readlink(ctx context.Context) ([]byte, syscall.Errno) { // TODO: cleanup localRepositoryPath, err := n.param.GitClient.FetchLocalRepositoryPath(n.source) if err != nil { - fmt.Println(err) + n.param.logger.Error(err.Error()) } return []byte(localRepositoryPath), 0 } diff --git a/fstree/root.go b/fstree/root.go index 0ba9b4f..bb49cca 100644 --- a/fstree/root.go +++ b/fstree/root.go @@ -3,6 +3,7 @@ package fstree import ( "context" "fmt" + "log/slog" "os" "os/signal" "syscall" @@ -34,6 +35,7 @@ type FSParam struct { GitClient GitClient GitPlatform GitPlatform + logger *slog.Logger staticInoChan chan uint64 } @@ -44,13 +46,14 @@ type rootNode struct { var _ = (fs.NodeOnAdder)((*rootNode)(nil)) -func Start(mountpoint string, mountoptions []string, param *FSParam, debug bool) error { - fmt.Printf("Mounting in %v\n", mountpoint) +func Start(logger *slog.Logger, mountpoint string, mountoptions []string, param *FSParam, debug bool) error { + logger.Info("Mounting", "mountpoint", mountpoint) opts := &fs.Options{} opts.MountOptions.Options = mountoptions opts.Debug = debug + param.logger = logger param.staticInoChan = make(chan uint64) root := &rootNode{ param: param, @@ -64,7 +67,7 @@ func Start(mountpoint string, mountoptions []string, param *FSParam, debug bool) } signalChan := make(chan os.Signal) - go signalHandler(signalChan, server) + go signalHandler(logger, signalChan, server) signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) // server.Serve() is already called in fs.Mount() so we shouldn't call it ourself. We wait for the server to terminate. @@ -92,7 +95,7 @@ func (n *rootNode) OnAdd(ctx context.Context) { n.AddChild(groupName, persistentInode, false) } - fmt.Println("Mounted and ready to use") + n.param.logger.Info("Mounted and ready to use") } func staticInoGenerator(staticInoChan chan<- uint64) { @@ -103,18 +106,18 @@ func staticInoGenerator(staticInoChan chan<- uint64) { } } -func signalHandler(signalChan <-chan os.Signal, server *fuse.Server) { +func signalHandler(logger *slog.Logger, signalChan <-chan os.Signal, server *fuse.Server) { err := server.WaitMount() if err != nil { - fmt.Printf("failed to start exit signal handler: %v\n", err) + logger.Error("failed to start exit signal handler", "error", err) return } for { s := <-signalChan - fmt.Printf("Caught %v: stopping\n", s) + logger.Info("Caught signal", "signal", s) err := server.Unmount() if err != nil { - fmt.Printf("Failed to unmount: %v\n", err) + logger.Error("Failed to unmount", "error", err) } } } diff --git a/git/client.go b/git/client.go index e1ee3f7..da90295 100644 --- a/git/client.go +++ b/git/client.go @@ -3,6 +3,7 @@ package git import ( "context" "fmt" + "log/slog" "os" "path/filepath" "regexp" @@ -28,6 +29,8 @@ type GitClientParam struct { type gitClient struct { GitClientParam + logger *slog.Logger + hostnameProg *regexp.Regexp majorVersion int @@ -39,12 +42,14 @@ type gitClient struct { pullTask *taskq.Task } -func NewClient(p GitClientParam) (*gitClient, error) { +func NewClient(logger *slog.Logger, p GitClientParam) (*gitClient, error) { queueFactory := memqueue.NewFactory() // Create the client c := &gitClient{ GitClientParam: p, + logger: logger, + hostnameProg: regexp.MustCompile(`([a-z0-1:\-]+\.)+[a-z0-1:\-]+`), queue: queueFactory.RegisterQueue(&taskq.QueueOptions{ @@ -56,7 +61,7 @@ func NewClient(p GitClientParam) (*gitClient, error) { } // Parse git version - gitVersionOutput, err := utils.ExecProcess("git", "--version") + gitVersionOutput, err := utils.ExecProcess(logger, "git", "--version") if err != nil { return nil, fmt.Errorf("failed to run \"git --version\": %v", err) } @@ -71,7 +76,7 @@ func NewClient(p GitClientParam) (*gitClient, error) { return nil, fmt.Errorf("failed to parse git minor version \"%v\": %v", gitVersionOutput, err) } c.patchVersion = gitVersionMatches[3] - fmt.Printf("Detected git version: major = %v minor = %v patch = %v\n", c.majorVersion, c.minorVersion, c.patchVersion) + logger.Info("Detected git version", "major", c.majorVersion, "minor", c.minorVersion, "patch", c.patchVersion) // Register tasks c.cloneTask = taskq.RegisterTask(&taskq.TaskOptions{ diff --git a/git/clone.go b/git/clone.go index 4efac57..3fbcb47 100644 --- a/git/clone.go +++ b/git/clone.go @@ -15,26 +15,27 @@ func (c *gitClient) clone(url string, defaultBranch string, dst string) error { // resulting in a very barebone local copy // Init the local repo - fmt.Printf("Initializing %v into %v\n", url, dst) + c.logger.Info("Initializing git repository", "directory", dst, "repository", url) args := []string{ "init", } if c.majorVersion > 2 || c.majorVersion == 2 && c.minorVersion >= 28 { args = append(args, "--initial-branch", defaultBranch) } else { - fmt.Printf("Version of git is too old to support --initial-branch. Consider upgrading git to version >= 2.28.0") + c.logger.Warn("Version of git is too old to support --initial-branch. Consider upgrading git to version >= 2.28.0") } args = append(args, "--", dst, // directory ) - _, err := utils.ExecProcess("git", args...) + _, err := utils.ExecProcess(c.logger, "git", args...) if err != nil { return fmt.Errorf("failed to init git repo %v to %v: %v", url, dst, err) } // Configure the remote _, err = utils.ExecProcessInDir( + c.logger, dst, // workdir "git", "remote", "add", "-m", defaultBranch, @@ -48,6 +49,7 @@ func (c *gitClient) clone(url string, defaultBranch string, dst string) error { // Configure the default branch _, err = utils.ExecProcessInDir( + c.logger, dst, // workdir "git", "config", "--local", "--", @@ -59,6 +61,7 @@ func (c *gitClient) clone(url string, defaultBranch string, dst string) error { return fmt.Errorf("failed to setup default branch remote in git repo %v: %v", dst, err) } _, err = utils.ExecProcessInDir( + c.logger, dst, // workdir "git", "config", "--local", "--", @@ -71,6 +74,7 @@ func (c *gitClient) clone(url string, defaultBranch string, dst string) error { } } else { // Clone the repo + c.logger.Info("Cloning git repository", "directory", dst, "repository", url) args := []string{ "clone", "--origin", c.GitClientParam.Remote, @@ -84,7 +88,7 @@ func (c *gitClient) clone(url string, defaultBranch string, dst string) error { dst, // directory ) - _, err := utils.ExecProcess("git", args...) + _, err := utils.ExecProcess(c.logger, "git", args...) if err != nil { return fmt.Errorf("failed to clone git repo %v to %v: %v", url, dst, err) } diff --git a/git/pull.go b/git/pull.go index 549ec69..ef5f9dd 100644 --- a/git/pull.go +++ b/git/pull.go @@ -10,6 +10,7 @@ import ( func (c *gitClient) pull(repoPath string, defaultBranch string) error { // Check if the local repo is on default branch branchName, err := utils.ExecProcessInDir( + c.logger, repoPath, // workdir "git", "branch", "--show-current", @@ -32,12 +33,12 @@ func (c *gitClient) pull(repoPath string, defaultBranch string) error { defaultBranch, // refspec ) - _, err = utils.ExecProcessInDir(repoPath, "git", args...) + _, err = utils.ExecProcessInDir(c.logger, repoPath, "git", args...) if err != nil { return fmt.Errorf("failed to pull git repo %v: %v", repoPath, err) } } else { - fmt.Printf("%v != %v, skipping pull", branchName, defaultBranch) + c.logger.Info("Skipping pull because local is not on default branch", "currentBranch", branchName, "defaultBranch", defaultBranch) } return nil diff --git a/main.go b/main.go index 1fd4c84..41c74c6 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "flag" "fmt" + "log/slog" "os" "path/filepath" "strings" @@ -110,6 +111,9 @@ func main() { os.Exit(1) } + // Get logger + logger := slog.Default() + // Configure mountpoint mountpoint := config.FS.Mountpoint if flag.NArg() == 1 { @@ -137,7 +141,7 @@ func main() { fmt.Println(err) os.Exit(1) } - gitClient, _ := git.NewClient(*gitClientParam) + gitClient, _ := git.NewClient(logger, *gitClientParam) // Create the gitlab client GitlabClientConfig, err := makeGitlabConfig(config) @@ -145,10 +149,11 @@ func main() { fmt.Println(err) os.Exit(1) } - gitlabClient, _ := gitlab.NewClient(config.Gitlab.URL, config.Gitlab.Token, *GitlabClientConfig) + gitlabClient, _ := gitlab.NewClient(logger, config.Gitlab.URL, config.Gitlab.Token, *GitlabClientConfig) // Start the filesystem err = fstree.Start( + logger, mountpoint, parsedMountoptions, &fstree.FSParam{GitClient: gitClient, GitPlatform: gitlabClient}, diff --git a/platforms/gitlab/client.go b/platforms/gitlab/client.go index ee95af3..7611ae4 100644 --- a/platforms/gitlab/client.go +++ b/platforms/gitlab/client.go @@ -2,6 +2,7 @@ package gitlab import ( "fmt" + "log/slog" "slices" "github.com/badjware/gitlabfs/fstree" @@ -26,6 +27,8 @@ type gitlabClient struct { GitlabClientConfig client *gitlab.Client + logger *slog.Logger + // root group cache rootGroupCache map[string]fstree.GroupSource currentUserCache *User @@ -35,7 +38,7 @@ type gitlabClient struct { userCache map[int]*User } -func NewClient(gitlabUrl string, gitlabToken string, p GitlabClientConfig) (*gitlabClient, error) { +func NewClient(logger *slog.Logger, gitlabUrl string, gitlabToken string, p GitlabClientConfig) (*gitlabClient, error) { client, err := gitlab.NewClient( gitlabToken, gitlab.WithBaseURL(gitlabUrl), @@ -48,6 +51,8 @@ func NewClient(gitlabUrl string, gitlabToken string, p GitlabClientConfig) (*git GitlabClientConfig: p, client: client, + logger: logger, + rootGroupCache: nil, currentUserCache: nil, @@ -82,9 +87,10 @@ func (c *gitlabClient) FetchRootGroupContent() (map[string]fstree.GroupSource, e if c.IncludeCurrentUser { currentUser, err := c.fetchCurrentUser() if err != nil { - return nil, err + c.logger.Warn(err.Error()) + } else { + rootGroupCache[currentUser.Name] = currentUser } - rootGroupCache[currentUser.Name] = currentUser } c.rootGroupCache = rootGroupCache diff --git a/utils/process.go b/utils/process.go index 49cfed0..d5f2fcc 100644 --- a/utils/process.go +++ b/utils/process.go @@ -1,7 +1,7 @@ package utils import ( - "fmt" + "log/slog" "os/exec" "strings" ) @@ -11,19 +11,19 @@ const ( stderr = "stderr" ) -func ExecProcessInDir(workdir string, command string, args ...string) (string, error) { +func ExecProcessInDir(logger *slog.Logger, workdir string, command string, args ...string) (string, error) { cmd := exec.Command(command, args...) if workdir != "" { cmd.Dir = workdir } // Run the command - fmt.Printf("%v %v\n", command, strings.Join(args, " ")) + logger.Debug("Running command", "cmd", command, "args", args) output, err := cmd.Output() return strings.TrimSpace(string(output)), err } -func ExecProcess(command string, args ...string) (string, error) { - return ExecProcessInDir("", command, args...) +func ExecProcess(logger *slog.Logger, command string, args ...string) (string, error) { + return ExecProcessInDir(logger, "", command, args...) } From 4667c12e47b359979c5c54a7426241583f880505 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Thu, 6 Jun 2024 21:46:29 -0400 Subject: [PATCH 09/33] fix regression that caused more queries then necessary to be made to gitlab api --- platforms/gitlab/group.go | 53 +++++++++++++++++++++++++++++++-------- platforms/gitlab/user.go | 3 +++ 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/platforms/gitlab/group.go b/platforms/gitlab/group.go index 89d3b3d..77bde8d 100644 --- a/platforms/gitlab/group.go +++ b/platforms/gitlab/group.go @@ -15,8 +15,8 @@ type Group struct { mux sync.Mutex // group content cache - groupCache map[string]fstree.GroupSource - projectCache map[string]fstree.RepositorySource + childGroups map[string]fstree.GroupSource + childRepository map[string]fstree.RepositorySource } func (g *Group) GetGroupID() uint64 { @@ -27,8 +27,8 @@ func (g *Group) InvalidateCache() { g.mux.Lock() defer g.mux.Unlock() - g.groupCache = nil - g.projectCache = nil + g.childGroups = nil + g.childRepository = nil } func (c *gitlabClient) fetchGroup(gid int) (*Group, error) { @@ -36,7 +36,10 @@ func (c *gitlabClient) fetchGroup(gid int) (*Group, error) { // TODO: cache invalidation? group, found := c.groupCache[gid] if found { + c.logger.Debug("Group cache hit", "gid", gid) return group, nil + } else { + c.logger.Debug("Group cache miss; fetching group", "gid", gid) } // If not in cache, fetch group infos from API @@ -44,12 +47,40 @@ func (c *gitlabClient) fetchGroup(gid int) (*Group, error) { if err != nil { return nil, fmt.Errorf("failed to fetch group with id %v: %v", gid, err) } + c.logger.Debug("Fetched group", "gid", gid) newGroup := Group{ ID: gitlabGroup.ID, Name: gitlabGroup.Path, - groupCache: nil, - projectCache: nil, + childGroups: nil, + childRepository: nil, + } + + // save in cache + c.groupCache[gid] = &newGroup + + return &newGroup, nil +} + +func (c *gitlabClient) newGroupFromGitlabGroup(gitlabGroup *gitlab.Group) (*Group, error) { + gid := gitlabGroup.ID + + // start by searching the cache + group, found := c.groupCache[gid] + if found { + c.logger.Debug("Group cache hit", "gid", gid) + return group, nil + } else { + c.logger.Debug("Group cache miss; registering group", "gid", gid) + } + + // if not in cache, convert and save to cache now + newGroup := Group{ + ID: gitlabGroup.ID, + Name: gitlabGroup.Path, + + childGroups: nil, + childRepository: nil, } // save in cache @@ -64,7 +95,7 @@ func (c *gitlabClient) fetchGroupContent(group *Group) (map[string]fstree.GroupS // Get cached data if available // TODO: cache cache invalidation? - if group.groupCache == nil || group.projectCache == nil { + if group.childGroups == nil || group.childRepository == nil { groupCache := make(map[string]fstree.GroupSource) projectCache := make(map[string]fstree.RepositorySource) @@ -82,7 +113,7 @@ func (c *gitlabClient) fetchGroupContent(group *Group) (map[string]fstree.GroupS return nil, nil, fmt.Errorf("failed to fetch groups in gitlab: %v", err) } for _, gitlabGroup := range gitlabGroups { - group, _ := c.fetchGroup(gitlabGroup.ID) + group, _ := c.newGroupFromGitlabGroup(gitlabGroup) groupCache[group.Name] = group } if response.CurrentPage >= response.TotalPages { @@ -114,8 +145,8 @@ func (c *gitlabClient) fetchGroupContent(group *Group) (map[string]fstree.GroupS listProjectOpt.Page = response.NextPage } - group.groupCache = groupCache - group.projectCache = projectCache + group.childGroups = groupCache + group.childRepository = projectCache } - return group.groupCache, group.projectCache, nil + return group.childGroups, group.childRepository, nil } diff --git a/platforms/gitlab/user.go b/platforms/gitlab/user.go index 480e35e..4dd7df0 100644 --- a/platforms/gitlab/user.go +++ b/platforms/gitlab/user.go @@ -34,7 +34,10 @@ func (c *gitlabClient) fetchUser(uid int) (*User, error) { // TODO: cache invalidation? user, found := c.userCache[uid] if found { + c.logger.Debug("User cache hit", "uid", uid) return user, nil + } else { + c.logger.Debug("User cache miss", "uid", uid) } // If not in cache, fetch group infos from API From e26f0ae86566c8404fe497b42891950e9026cdc2 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Thu, 6 Jun 2024 22:01:22 -0400 Subject: [PATCH 10/33] fix cache invalidation --- platforms/gitlab/group.go | 45 +++++++++++++++++++++++++-------------- platforms/gitlab/user.go | 21 +++++++++--------- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/platforms/gitlab/group.go b/platforms/gitlab/group.go index 77bde8d..3ba961a 100644 --- a/platforms/gitlab/group.go +++ b/platforms/gitlab/group.go @@ -12,11 +12,13 @@ type Group struct { ID int Name string + gitlabClient *gitlabClient + mux sync.Mutex - // group content cache - childGroups map[string]fstree.GroupSource - childRepository map[string]fstree.RepositorySource + // group content + childGroups map[string]fstree.GroupSource + childProjects map[string]fstree.RepositorySource } func (g *Group) GetGroupID() uint64 { @@ -27,8 +29,15 @@ func (g *Group) InvalidateCache() { g.mux.Lock() defer g.mux.Unlock() + // clear child group from cache + for _, childGroup := range g.childGroups { + gid := int(childGroup.GetGroupID()) + delete(g.gitlabClient.groupCache, gid) + } + g.childGroups = nil + + // clear child repositories from cache g.childGroups = nil - g.childRepository = nil } func (c *gitlabClient) fetchGroup(gid int) (*Group, error) { @@ -52,8 +61,10 @@ func (c *gitlabClient) fetchGroup(gid int) (*Group, error) { ID: gitlabGroup.ID, Name: gitlabGroup.Path, - childGroups: nil, - childRepository: nil, + gitlabClient: c, + + childGroups: nil, + childProjects: nil, } // save in cache @@ -79,8 +90,10 @@ func (c *gitlabClient) newGroupFromGitlabGroup(gitlabGroup *gitlab.Group) (*Grou ID: gitlabGroup.ID, Name: gitlabGroup.Path, - childGroups: nil, - childRepository: nil, + gitlabClient: c, + + childGroups: nil, + childProjects: nil, } // save in cache @@ -95,9 +108,9 @@ func (c *gitlabClient) fetchGroupContent(group *Group) (map[string]fstree.GroupS // Get cached data if available // TODO: cache cache invalidation? - if group.childGroups == nil || group.childRepository == nil { - groupCache := make(map[string]fstree.GroupSource) - projectCache := make(map[string]fstree.RepositorySource) + if group.childGroups == nil || group.childProjects == nil { + childGroups := make(map[string]fstree.GroupSource) + childProjects := make(map[string]fstree.RepositorySource) // List subgroups in path ListGroupsOpt := &gitlab.ListSubgroupsOptions{ @@ -114,7 +127,7 @@ func (c *gitlabClient) fetchGroupContent(group *Group) (map[string]fstree.GroupS } for _, gitlabGroup := range gitlabGroups { group, _ := c.newGroupFromGitlabGroup(gitlabGroup) - groupCache[group.Name] = group + childGroups[group.Name] = group } if response.CurrentPage >= response.TotalPages { break @@ -136,7 +149,7 @@ func (c *gitlabClient) fetchGroupContent(group *Group) (map[string]fstree.GroupS } for _, gitlabProject := range gitlabProjects { project := c.newProjectFromGitlabProject(gitlabProject) - projectCache[project.Name] = &project + childProjects[project.Name] = &project } if response.CurrentPage >= response.TotalPages { break @@ -145,8 +158,8 @@ func (c *gitlabClient) fetchGroupContent(group *Group) (map[string]fstree.GroupS listProjectOpt.Page = response.NextPage } - group.childGroups = groupCache - group.childRepository = projectCache + group.childGroups = childGroups + group.childProjects = childProjects } - return group.childGroups, group.childRepository, nil + return group.childGroups, group.childProjects, nil } diff --git a/platforms/gitlab/user.go b/platforms/gitlab/user.go index 4dd7df0..fef1513 100644 --- a/platforms/gitlab/user.go +++ b/platforms/gitlab/user.go @@ -14,8 +14,8 @@ type User struct { mux sync.Mutex - // user content cache - projectCache map[string]fstree.RepositorySource + // user content + childProjects map[string]fstree.RepositorySource } func (u *User) GetGroupID() uint64 { @@ -26,7 +26,8 @@ func (u *User) InvalidateCache() { u.mux.Lock() defer u.mux.Unlock() - u.projectCache = nil + // clear child repositories from cache + u.childProjects = nil } func (c *gitlabClient) fetchUser(uid int) (*User, error) { @@ -49,7 +50,7 @@ func (c *gitlabClient) fetchUser(uid int) (*User, error) { ID: gitlabUser.ID, Name: gitlabUser.Username, - projectCache: nil, + childProjects: nil, } // save in cache @@ -68,7 +69,7 @@ func (c *gitlabClient) fetchCurrentUser() (*User, error) { ID: gitlabUser.ID, Name: gitlabUser.Username, - projectCache: nil, + childProjects: nil, } c.currentUserCache = &newUser } @@ -81,8 +82,8 @@ func (c *gitlabClient) fetchUserContent(user *User) (map[string]fstree.GroupSour // Get cached data if available // TODO: cache cache invalidation? - if user.projectCache == nil { - projectCache := make(map[string]fstree.RepositorySource) + if user.childProjects == nil { + childProjects := make(map[string]fstree.RepositorySource) // Fetch the user repositories listProjectOpt := &gitlab.ListProjectsOptions{ @@ -97,7 +98,7 @@ func (c *gitlabClient) fetchUserContent(user *User) (map[string]fstree.GroupSour } for _, gitlabProject := range gitlabProjects { project := c.newProjectFromGitlabProject(gitlabProject) - projectCache[project.Name] = &project + childProjects[project.Name] = &project } if response.CurrentPage >= response.TotalPages { break @@ -106,7 +107,7 @@ func (c *gitlabClient) fetchUserContent(user *User) (map[string]fstree.GroupSour listProjectOpt.Page = response.NextPage } - user.projectCache = projectCache + user.childProjects = childProjects } - return make(map[string]fstree.GroupSource), user.projectCache, nil + return make(map[string]fstree.GroupSource), user.childProjects, nil } From a4ed751abd2e89fe539b145d4f2670e157a4587e Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Fri, 7 Jun 2024 23:36:45 -0400 Subject: [PATCH 11/33] move config loader to its own package --- config/loader.go | 89 +++++++++++++++++++++++++++++++++++++++++++++ main.go | 93 ++++-------------------------------------------- 2 files changed, 96 insertions(+), 86 deletions(-) create mode 100644 config/loader.go diff --git a/config/loader.go b/config/loader.go new file mode 100644 index 0000000..a0d244e --- /dev/null +++ b/config/loader.go @@ -0,0 +1,89 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/badjware/gitlabfs/git" + "github.com/badjware/gitlabfs/platforms/gitlab" + "gopkg.in/yaml.v2" +) + +type ( + Config struct { + FS FSConfig `yaml:"fs,omitempty"` + Gitlab gitlab.GitlabClientConfig `yaml:"gitlab,omitempty"` + Git git.GitClientParam `yaml:"git,omitempty"` + } + FSConfig struct { + Mountpoint string `yaml:"mountpoint,omitempty"` + MountOptions string `yaml:"mountoptions,omitempty"` + } +) + +func LoadConfig(configPath string) (*Config, error) { + // defaults + dataHome := os.Getenv("XDG_DATA_HOME") + if dataHome == "" { + dataHome = filepath.Join(os.Getenv("HOME"), ".local/share") + } + defaultCloneLocation := filepath.Join(dataHome, "gitlabfs") + + config := &Config{ + FS: FSConfig{ + Mountpoint: "", + MountOptions: "nodev,nosuid", + }, + Gitlab: gitlab.GitlabClientConfig{ + URL: "https://gitlab.com", + Token: "", + GroupIDs: []int{9970}, + UserIDs: []int{}, + IncludeCurrentUser: true, + PullMethod: "http", + }, + Git: git.GitClientParam{ + CloneLocation: defaultCloneLocation, + Remote: "origin", + OnClone: "init", + AutoPull: false, + Depth: 0, + QueueSize: 200, + QueueWorkerCount: 5, + }, + } + + if configPath != "" { + f, err := os.Open(configPath) + if err != nil { + return nil, fmt.Errorf("failed to open config file: %v", err) + } + defer f.Close() + + d := yaml.NewDecoder(f) + if err := d.Decode(config); err != nil { + return nil, fmt.Errorf("failed to parse config file: %v", err) + } + } + + return config, nil +} + +func MakeGitConfig(config *Config) (*git.GitClientParam, error) { + // parse on_clone + if config.Git.OnClone != "init" && config.Git.OnClone != "clone" { + return nil, fmt.Errorf("on_clone must be either \"init\" or \"clone\"") + } + + return &config.Git, nil +} + +func MakeGitlabConfig(config *Config) (*gitlab.GitlabClientConfig, error) { + // parse pull_method + if config.Gitlab.PullMethod != gitlab.PullMethodHTTP && config.Gitlab.PullMethod != gitlab.PullMethodSSH { + return nil, fmt.Errorf("pull_method must be either \"%v\" or \"%v\"", gitlab.PullMethodHTTP, gitlab.PullMethodSSH) + } + + return &config.Gitlab, nil +} diff --git a/main.go b/main.go index 41c74c6..6cb9626 100644 --- a/main.go +++ b/main.go @@ -5,93 +5,14 @@ import ( "fmt" "log/slog" "os" - "path/filepath" "strings" + "github.com/badjware/gitlabfs/config" "github.com/badjware/gitlabfs/fstree" "github.com/badjware/gitlabfs/git" "github.com/badjware/gitlabfs/platforms/gitlab" - "gopkg.in/yaml.v2" ) -type ( - Config struct { - FS FSConfig `yaml:"fs,omitempty"` - Gitlab gitlab.GitlabClientConfig `yaml:"gitlab,omitempty"` - Git git.GitClientParam `yaml:"git,omitempty"` - } - FSConfig struct { - Mountpoint string `yaml:"mountpoint,omitempty"` - MountOptions string `yaml:"mountoptions,omitempty"` - } -) - -func loadConfig(configPath string) (*Config, error) { - // defaults - dataHome := os.Getenv("XDG_DATA_HOME") - if dataHome == "" { - dataHome = filepath.Join(os.Getenv("HOME"), ".local/share") - } - defaultCloneLocation := filepath.Join(dataHome, "gitlabfs") - - config := &Config{ - FS: FSConfig{ - Mountpoint: "", - MountOptions: "nodev,nosuid", - }, - Gitlab: gitlab.GitlabClientConfig{ - URL: "https://gitlab.com", - Token: "", - GroupIDs: []int{9970}, - UserIDs: []int{}, - IncludeCurrentUser: true, - PullMethod: "http", - }, - Git: git.GitClientParam{ - CloneLocation: defaultCloneLocation, - Remote: "origin", - OnClone: "init", - AutoPull: false, - Depth: 0, - QueueSize: 200, - QueueWorkerCount: 5, - }, - } - - if configPath != "" { - f, err := os.Open(configPath) - if err != nil { - return nil, fmt.Errorf("failed to open config file: %v", err) - } - defer f.Close() - - d := yaml.NewDecoder(f) - if err := d.Decode(config); err != nil { - return nil, fmt.Errorf("failed to parse config file: %v", err) - } - } - - return config, nil -} - -func makeGitlabConfig(config *Config) (*gitlab.GitlabClientConfig, error) { - // parse pull_method - if config.Gitlab.PullMethod != gitlab.PullMethodHTTP && config.Gitlab.PullMethod != gitlab.PullMethodSSH { - return nil, fmt.Errorf("pull_method must be either \"%v\" or \"%v\"", gitlab.PullMethodHTTP, gitlab.PullMethodSSH) - } - - return &config.Gitlab, nil -} - -func makeGitConfig(config *Config) (*git.GitClientParam, error) { - // parse on_clone - if config.Git.OnClone != "init" && config.Git.OnClone != "clone" { - return nil, fmt.Errorf("on_clone must be either \"init\" or \"clone\"") - } - - return &config.Git, nil -} - func main() { configPath := flag.String("config", "", "The config file") mountoptionsFlag := flag.String("o", "", "Filesystem mount options. See mount.fuse(8)") @@ -105,7 +26,7 @@ func main() { } flag.Parse() - config, err := loadConfig(*configPath) + loadedConfig, err := config.LoadConfig(*configPath) if err != nil { fmt.Println(err) os.Exit(1) @@ -115,7 +36,7 @@ func main() { logger := slog.Default() // Configure mountpoint - mountpoint := config.FS.Mountpoint + mountpoint := loadedConfig.FS.Mountpoint if flag.NArg() == 1 { mountpoint = flag.Arg(0) } @@ -126,7 +47,7 @@ func main() { } // Configure mountoptions - mountoptions := config.FS.MountOptions + mountoptions := loadedConfig.FS.MountOptions if *mountoptionsFlag != "" { mountoptions = *mountoptionsFlag } @@ -136,7 +57,7 @@ func main() { } // Create the git client - gitClientParam, err := makeGitConfig(config) + gitClientParam, err := config.MakeGitConfig(loadedConfig) if err != nil { fmt.Println(err) os.Exit(1) @@ -144,12 +65,12 @@ func main() { gitClient, _ := git.NewClient(logger, *gitClientParam) // Create the gitlab client - GitlabClientConfig, err := makeGitlabConfig(config) + GitlabClientConfig, err := config.MakeGitlabConfig(loadedConfig) if err != nil { fmt.Println(err) os.Exit(1) } - gitlabClient, _ := gitlab.NewClient(logger, config.Gitlab.URL, config.Gitlab.Token, *GitlabClientConfig) + gitlabClient, _ := gitlab.NewClient(logger, loadedConfig.Gitlab.URL, loadedConfig.Gitlab.Token, *GitlabClientConfig) // Start the filesystem err = fstree.Start( From 0a501582394c63581d52dacb19777e3bf3aeaf89 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Sat, 8 Jun 2024 01:02:35 -0400 Subject: [PATCH 12/33] add config tests --- config.example.yaml | 12 +-- config/config.test.yaml | 22 ++++++ config/loader.go | 2 +- config/loader_test.go | 164 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 193 insertions(+), 7 deletions(-) create mode 100644 config/config.test.yaml create mode 100644 config/loader_test.go diff --git a/config.example.yaml b/config.example.yaml index 9bd2843..2634bd2 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -14,6 +14,12 @@ gitlab: # Default to anonymous (only public projects will be visible). #token: + # Must be set to either "http" or "ssh". + # The protocol to configure the git remote on. + # "http" may not work on private repos unless a credential manager is configured + # If possible, prefer "ssh" over "http" + pull_method: http + # A list of the group ids to expose their projects in the filesystem. group_ids: - 9970 # gitlab-org @@ -32,12 +38,6 @@ git: # The name of the remote in the local clone. remote: origin - # Must be set to either "http" or "ssh". - # The protocol to configure the git remote on. - # "http" may not work on private repos unless a credential manager is configured - # If possible, prefer "ssh" over "http" - pull_method: http - # Must be set to either "init", or "clone". # If set to "init", the local copy will be initialized with `git init` and the remote is configured manually. The git server is nerver queried. (fast) # If set to "clone", the local copy will be initialized with `git clone`. (slow) diff --git a/config/config.test.yaml b/config/config.test.yaml new file mode 100644 index 0000000..e8a0f3d --- /dev/null +++ b/config/config.test.yaml @@ -0,0 +1,22 @@ +fs: + mountpoint: /tmp/gitlabfs/test/mnt + mountoptions: nodev + +gitlab: + url: https://example.com + token: "12345" + group_ids: + - 123 + user_ids: + - 456 + include_current_user: true + pull_method: ssh + +git: + clone_location: /tmp/gitlabfs/test/clone + remote: origin + on_clone: clone + auto_pull: false + depth: 0 + queue_size: 100 + worker_count: 1 \ No newline at end of file diff --git a/config/loader.go b/config/loader.go index a0d244e..d9002e9 100644 --- a/config/loader.go +++ b/config/loader.go @@ -38,10 +38,10 @@ func LoadConfig(configPath string) (*Config, error) { Gitlab: gitlab.GitlabClientConfig{ URL: "https://gitlab.com", Token: "", + PullMethod: "http", GroupIDs: []int{9970}, UserIDs: []int{}, IncludeCurrentUser: true, - PullMethod: "http", }, Git: git.GitClientParam{ CloneLocation: defaultCloneLocation, diff --git a/config/loader_test.go b/config/loader_test.go new file mode 100644 index 0000000..49e259e --- /dev/null +++ b/config/loader_test.go @@ -0,0 +1,164 @@ +package config_test + +import ( + "reflect" + "testing" + + "github.com/badjware/gitlabfs/config" + "github.com/badjware/gitlabfs/git" + "github.com/badjware/gitlabfs/platforms/gitlab" +) + +func TestLoadConfig(t *testing.T) { + tests := map[string]struct { + input string + expected *config.Config + }{ + "LoadConfig": { + input: "config.test.yaml", + expected: &config.Config{ + FS: config.FSConfig{ + Mountpoint: "/tmp/gitlabfs/test/mnt", + MountOptions: "nodev", + }, + Gitlab: gitlab.GitlabClientConfig{ + URL: "https://example.com", + Token: "12345", + PullMethod: "ssh", + GroupIDs: []int{123}, + UserIDs: []int{456}, + IncludeCurrentUser: true, + }, + Git: git.GitClientParam{ + CloneLocation: "/tmp/gitlabfs/test/clone", + Remote: "origin", + OnClone: "clone", + AutoPull: false, + Depth: 0, + QueueSize: 100, + QueueWorkerCount: 1, + }}, + }, + } + + for name, test := range tests { + test := test + t.Run(name, func(t *testing.T) { + t.Parallel() + got, err := config.LoadConfig(test.input) + expected := test.expected + if !reflect.DeepEqual(got, expected) { + t.Fatalf("LoadConfig(%v) returned %v; expected %v; error: %v", test.input, got, expected, err) + } + }) + } +} + +func TestMakeGitConfig(t *testing.T) { + tests := map[string]struct { + input *config.Config + expected *git.GitClientParam + }{ + "ValidConfig": { + input: &config.Config{ + Git: git.GitClientParam{ + CloneLocation: "/tmp", + Remote: "origin", + OnClone: "init", + AutoPull: false, + Depth: 0, + QueueSize: 200, + QueueWorkerCount: 5, + }, + }, + expected: &git.GitClientParam{ + CloneLocation: "/tmp", + Remote: "origin", + OnClone: "init", + AutoPull: false, + Depth: 0, + QueueSize: 200, + QueueWorkerCount: 5, + }, + }, + "InvalidOnClone": { + input: &config.Config{ + Git: git.GitClientParam{ + CloneLocation: "/tmp", + Remote: "origin", + OnClone: "invalid", + AutoPull: false, + Depth: 0, + QueueSize: 200, + QueueWorkerCount: 5, + }, + }, + expected: nil, + }, + } + + for name, test := range tests { + test := test + t.Run(name, func(t *testing.T) { + t.Parallel() + got, err := config.MakeGitConfig(test.input) + expected := test.expected + if !reflect.DeepEqual(got, expected) { + t.Fatalf("MakeGitConfig(%v) returned %v; expected %v; error %v", test.input, got, expected, err) + } + }) + } +} + +func TestMakeGitlabConfig(t *testing.T) { + tests := map[string]struct { + input *config.Config + expected *gitlab.GitlabClientConfig + }{ + "ValidConfig": { + input: &config.Config{ + Gitlab: gitlab.GitlabClientConfig{ + URL: "https://gitlab.com", + Token: "", + GroupIDs: []int{9970}, + UserIDs: []int{}, + IncludeCurrentUser: true, + PullMethod: "http", + }, + }, + expected: &gitlab.GitlabClientConfig{ + URL: "https://gitlab.com", + Token: "", + GroupIDs: []int{9970}, + UserIDs: []int{}, + IncludeCurrentUser: true, + PullMethod: "http", + }, + }, + "InvalidPullMethod": { + input: &config.Config{ + Gitlab: gitlab.GitlabClientConfig{ + URL: "https://gitlab.com", + Token: "", + GroupIDs: []int{9970}, + UserIDs: []int{}, + IncludeCurrentUser: true, + PullMethod: "invalid", + }, + }, + expected: nil, + }, + } + + for name, test := range tests { + test := test + t.Run(name, func(t *testing.T) { + t.Parallel() + got, err := config.MakeGitlabConfig(test.input) + expected := test.expected + if !reflect.DeepEqual(got, expected) { + t.Fatalf("MakeGitlabConfig(%v) returned %v; expected %v; error: %v", test.input, got, expected, err) + } + }) + } +} From 6d0d3fdfc0be646c20c7837ca99192be6cd77beb Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Wed, 17 Jul 2024 23:35:18 -0400 Subject: [PATCH 13/33] guard gitlab cache Map with RWMutex to prevent concurrent read/write fixes #11 --- fstree/group.go | 2 +- fstree/refresh.go | 2 +- platforms/gitlab/client.go | 7 +++++-- platforms/gitlab/group.go | 20 +++++++++++++++++--- platforms/gitlab/user.go | 13 ++++++++++--- 5 files changed, 34 insertions(+), 10 deletions(-) diff --git a/fstree/group.go b/fstree/group.go index a820e62..a60337c 100644 --- a/fstree/group.go +++ b/fstree/group.go @@ -18,7 +18,7 @@ type groupNode struct { type GroupSource interface { GetGroupID() uint64 - InvalidateCache() + InvalidateContentCache() } // Ensure we are implementing the NodeReaddirer interface diff --git a/fstree/refresh.go b/fstree/refresh.go index 71cfdcc..ebf6fe5 100644 --- a/fstree/refresh.go +++ b/fstree/refresh.go @@ -41,6 +41,6 @@ func (n *refreshNode) Setattr(ctx context.Context, fh fs.FileHandle, in *fuse.Se } func (n *refreshNode) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { - n.source.InvalidateCache() + n.source.InvalidateContentCache() return nil, 0, 0 } diff --git a/platforms/gitlab/client.go b/platforms/gitlab/client.go index 7611ae4..fc885c5 100644 --- a/platforms/gitlab/client.go +++ b/platforms/gitlab/client.go @@ -4,6 +4,7 @@ import ( "fmt" "log/slog" "slices" + "sync" "github.com/badjware/gitlabfs/fstree" "github.com/xanzy/go-gitlab" @@ -34,8 +35,10 @@ type gitlabClient struct { currentUserCache *User // API response cache - groupCache map[int]*Group - userCache map[int]*User + groupCacheMux sync.RWMutex + groupCache map[int]*Group + userCacheMux sync.RWMutex + userCache map[int]*User } func NewClient(logger *slog.Logger, gitlabUrl string, gitlabToken string, p GitlabClientConfig) (*gitlabClient, error) { diff --git a/platforms/gitlab/group.go b/platforms/gitlab/group.go index 3ba961a..417f904 100644 --- a/platforms/gitlab/group.go +++ b/platforms/gitlab/group.go @@ -16,7 +16,7 @@ type Group struct { mux sync.Mutex - // group content + // hold group content childGroups map[string]fstree.GroupSource childProjects map[string]fstree.RepositorySource } @@ -25,15 +25,17 @@ func (g *Group) GetGroupID() uint64 { return uint64(g.ID) } -func (g *Group) InvalidateCache() { +func (g *Group) InvalidateContentCache() { g.mux.Lock() defer g.mux.Unlock() // clear child group from cache + g.gitlabClient.groupCacheMux.Lock() for _, childGroup := range g.childGroups { gid := int(childGroup.GetGroupID()) delete(g.gitlabClient.groupCache, gid) } + g.gitlabClient.groupCacheMux.Unlock() g.childGroups = nil // clear child repositories from cache @@ -43,7 +45,9 @@ func (g *Group) InvalidateCache() { func (c *gitlabClient) fetchGroup(gid int) (*Group, error) { // start by searching the cache // TODO: cache invalidation? + c.groupCacheMux.RLock() group, found := c.groupCache[gid] + c.groupCacheMux.RUnlock() if found { c.logger.Debug("Group cache hit", "gid", gid) return group, nil @@ -68,7 +72,9 @@ func (c *gitlabClient) fetchGroup(gid int) (*Group, error) { } // save in cache + c.groupCacheMux.Lock() c.groupCache[gid] = &newGroup + c.groupCacheMux.Unlock() return &newGroup, nil } @@ -77,15 +83,18 @@ func (c *gitlabClient) newGroupFromGitlabGroup(gitlabGroup *gitlab.Group) (*Grou gid := gitlabGroup.ID // start by searching the cache + c.groupCacheMux.RLock() group, found := c.groupCache[gid] + c.groupCacheMux.RUnlock() if found { + // if found in cache, return the cached reference c.logger.Debug("Group cache hit", "gid", gid) return group, nil } else { c.logger.Debug("Group cache miss; registering group", "gid", gid) } - // if not in cache, convert and save to cache now + // if not found in cache, convert and save to cache now newGroup := Group{ ID: gitlabGroup.ID, Name: gitlabGroup.Path, @@ -97,12 +106,17 @@ func (c *gitlabClient) newGroupFromGitlabGroup(gitlabGroup *gitlab.Group) (*Grou } // save in cache + c.groupCacheMux.Lock() c.groupCache[gid] = &newGroup + c.groupCacheMux.Unlock() return &newGroup, nil } func (c *gitlabClient) fetchGroupContent(group *Group) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) { + // Only a single routine can fetch the group content at the time. + // We lock for the whole duration of the function to avoid fetching the same data from the API + // multiple times if concurrent calls where to occur. group.mux.Lock() defer group.mux.Unlock() diff --git a/platforms/gitlab/user.go b/platforms/gitlab/user.go index fef1513..75acd9d 100644 --- a/platforms/gitlab/user.go +++ b/platforms/gitlab/user.go @@ -14,7 +14,7 @@ type User struct { mux sync.Mutex - // user content + // hold user content childProjects map[string]fstree.RepositorySource } @@ -22,7 +22,7 @@ func (u *User) GetGroupID() uint64 { return uint64(u.ID) } -func (u *User) InvalidateCache() { +func (u *User) InvalidateContentCache() { u.mux.Lock() defer u.mux.Unlock() @@ -33,15 +33,18 @@ func (u *User) InvalidateCache() { func (c *gitlabClient) fetchUser(uid int) (*User, error) { // start by searching the cache // TODO: cache invalidation? + c.userCacheMux.RLock() user, found := c.userCache[uid] + c.userCacheMux.RUnlock() if found { + // if found in cache, return the cached reference c.logger.Debug("User cache hit", "uid", uid) return user, nil } else { c.logger.Debug("User cache miss", "uid", uid) } - // If not in cache, fetch group infos from API + // If not found in cache, fetch group infos from API gitlabUser, _, err := c.client.Users.GetUser(uid) if err != nil { return nil, fmt.Errorf("failed to fetch user with id %v: %v", uid, err) @@ -55,6 +58,7 @@ func (c *gitlabClient) fetchUser(uid int) (*User, error) { // save in cache c.userCache[uid] = &newUser + c.userCacheMux.Unlock() return &newUser, nil } @@ -77,6 +81,9 @@ func (c *gitlabClient) fetchCurrentUser() (*User, error) { } func (c *gitlabClient) fetchUserContent(user *User) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) { + // Only a single routine can fetch the user content at the time. + // We lock for the whole duration of the function to avoid fetching the same data from the API + // multiple times if concurrent calls where to occur. user.mux.Lock() defer user.mux.Unlock() From 2d0a62dc459274ced1cb24cbf142a5bd2b3b6946 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Thu, 18 Jul 2024 00:25:13 -0400 Subject: [PATCH 14/33] hide archived project by default prefix archived project name with a "." by default so they appear hidden on the filesystem. At the same time, added the configuration gitlab.archived_project_handling to set how archived projects are handled --- config.example.yaml | 7 ++++ config/loader.go | 18 ++++++---- config/loader_test.go | 66 +++++++++++++++++++++++-------------- platforms/gitlab/client.go | 17 ++++++---- platforms/gitlab/group.go | 4 ++- platforms/gitlab/project.go | 16 ++++++--- platforms/gitlab/user.go | 4 ++- 7 files changed, 90 insertions(+), 42 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index 2634bd2..f80d607 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -27,6 +27,13 @@ gitlab: # A list of the user ids to expose their personal projects in the filesystem. user_ids: [] + # Set how archived projects are handled. + # If set to "show", it will add them to the filesystem and treat them like any other repository + # If set to "hide", it will add them to the filesystem, but prefix the symlink with a "." + # If set to "ignore", it will make them absent from the filesystem + # Default to "hide" + archived_project_handling: hide + # If set to true, the user the api token belongs to will automatically be added to the list of users exposed by the filesystem. include_current_user: true diff --git a/config/loader.go b/config/loader.go index d9002e9..b0d5b5d 100644 --- a/config/loader.go +++ b/config/loader.go @@ -36,12 +36,13 @@ func LoadConfig(configPath string) (*Config, error) { MountOptions: "nodev,nosuid", }, Gitlab: gitlab.GitlabClientConfig{ - URL: "https://gitlab.com", - Token: "", - PullMethod: "http", - GroupIDs: []int{9970}, - UserIDs: []int{}, - IncludeCurrentUser: true, + URL: "https://gitlab.com", + Token: "", + PullMethod: "http", + GroupIDs: []int{9970}, + UserIDs: []int{}, + ArchivedProjectHandling: "hide", + IncludeCurrentUser: true, }, Git: git.GitClientParam{ CloneLocation: defaultCloneLocation, @@ -85,5 +86,10 @@ func MakeGitlabConfig(config *Config) (*gitlab.GitlabClientConfig, error) { return nil, fmt.Errorf("pull_method must be either \"%v\" or \"%v\"", gitlab.PullMethodHTTP, gitlab.PullMethodSSH) } + // parse archive_handing + if config.Gitlab.ArchivedProjectHandling != gitlab.ArchivedProjectShow && config.Gitlab.ArchivedProjectHandling != gitlab.ArchivedProjectHide && config.Gitlab.ArchivedProjectHandling != gitlab.ArchivedProjectIgnore { + return nil, fmt.Errorf("pull_method must be either \"%v\", \"%v\" or \"%v\"", gitlab.ArchivedProjectShow, gitlab.ArchivedProjectHide, gitlab.ArchivedProjectIgnore) + } + return &config.Gitlab, nil } diff --git a/config/loader_test.go b/config/loader_test.go index 49e259e..0d6bf95 100644 --- a/config/loader_test.go +++ b/config/loader_test.go @@ -22,12 +22,13 @@ func TestLoadConfig(t *testing.T) { MountOptions: "nodev", }, Gitlab: gitlab.GitlabClientConfig{ - URL: "https://example.com", - Token: "12345", - PullMethod: "ssh", - GroupIDs: []int{123}, - UserIDs: []int{456}, - IncludeCurrentUser: true, + URL: "https://example.com", + Token: "12345", + PullMethod: "ssh", + GroupIDs: []int{123}, + UserIDs: []int{456}, + ArchivedProjectHandling: "hide", + IncludeCurrentUser: true, }, Git: git.GitClientParam{ CloneLocation: "/tmp/gitlabfs/test/clone", @@ -118,32 +119,49 @@ func TestMakeGitlabConfig(t *testing.T) { "ValidConfig": { input: &config.Config{ Gitlab: gitlab.GitlabClientConfig{ - URL: "https://gitlab.com", - Token: "", - GroupIDs: []int{9970}, - UserIDs: []int{}, - IncludeCurrentUser: true, - PullMethod: "http", + URL: "https://gitlab.com", + PullMethod: "http", + Token: "", + GroupIDs: []int{9970}, + UserIDs: []int{}, + ArchivedProjectHandling: "hide", + IncludeCurrentUser: true, }, }, expected: &gitlab.GitlabClientConfig{ - URL: "https://gitlab.com", - Token: "", - GroupIDs: []int{9970}, - UserIDs: []int{}, - IncludeCurrentUser: true, - PullMethod: "http", + URL: "https://gitlab.com", + PullMethod: "http", + Token: "", + GroupIDs: []int{9970}, + UserIDs: []int{}, + ArchivedProjectHandling: "hide", + IncludeCurrentUser: true, }, }, "InvalidPullMethod": { input: &config.Config{ Gitlab: gitlab.GitlabClientConfig{ - URL: "https://gitlab.com", - Token: "", - GroupIDs: []int{9970}, - UserIDs: []int{}, - IncludeCurrentUser: true, - PullMethod: "invalid", + URL: "https://gitlab.com", + PullMethod: "invalid", + Token: "", + GroupIDs: []int{9970}, + UserIDs: []int{}, + ArchivedProjectHandling: "hide", + IncludeCurrentUser: true, + }, + }, + expected: nil, + }, + "InvalidArchiveHandling": { + input: &config.Config{ + Gitlab: gitlab.GitlabClientConfig{ + URL: "https://gitlab.com", + PullMethod: "http", + Token: "", + GroupIDs: []int{9970}, + UserIDs: []int{}, + IncludeCurrentUser: true, + ArchivedProjectHandling: "invalid", }, }, expected: nil, diff --git a/platforms/gitlab/client.go b/platforms/gitlab/client.go index fc885c5..d7d99f0 100644 --- a/platforms/gitlab/client.go +++ b/platforms/gitlab/client.go @@ -13,15 +13,20 @@ import ( const ( PullMethodHTTP = "http" PullMethodSSH = "ssh" + + ArchivedProjectShow = "show" + ArchivedProjectHide = "hide" + ArchivedProjectIgnore = "ignore" ) type GitlabClientConfig struct { - URL string `yaml:"url,omitempty"` - Token string `yaml:"token,omitempty"` - GroupIDs []int `yaml:"group_ids,omitempty"` - UserIDs []int `yaml:"user_ids,omitempty"` - IncludeCurrentUser bool `yaml:"include_current_user,omitempty"` - PullMethod string `yaml:"pull_method,omitempty"` + URL string `yaml:"url,omitempty"` + Token string `yaml:"token,omitempty"` + GroupIDs []int `yaml:"group_ids,omitempty"` + UserIDs []int `yaml:"user_ids,omitempty"` + ArchivedProjectHandling string `yaml:"archived_project_handling,omitempty"` + IncludeCurrentUser bool `yaml:"include_current_user,omitempty"` + PullMethod string `yaml:"pull_method,omitempty"` } type gitlabClient struct { diff --git a/platforms/gitlab/group.go b/platforms/gitlab/group.go index 417f904..37d5507 100644 --- a/platforms/gitlab/group.go +++ b/platforms/gitlab/group.go @@ -163,7 +163,9 @@ func (c *gitlabClient) fetchGroupContent(group *Group) (map[string]fstree.GroupS } for _, gitlabProject := range gitlabProjects { project := c.newProjectFromGitlabProject(gitlabProject) - childProjects[project.Name] = &project + if project != nil { + childProjects[project.Path] = project + } } if response.CurrentPage >= response.TotalPages { break diff --git a/platforms/gitlab/project.go b/platforms/gitlab/project.go index 8a5416c..af366a2 100644 --- a/platforms/gitlab/project.go +++ b/platforms/gitlab/project.go @@ -1,12 +1,14 @@ package gitlab import ( + "path" + "github.com/xanzy/go-gitlab" ) type Project struct { ID int - Name string + Path string CloneURL string DefaultBranch string } @@ -23,11 +25,14 @@ func (p *Project) GetDefaultBranch() string { return p.DefaultBranch } -func (c *gitlabClient) newProjectFromGitlabProject(project *gitlab.Project) Project { +func (c *gitlabClient) newProjectFromGitlabProject(project *gitlab.Project) *Project { // https://godoc.org/github.com/xanzy/go-gitlab#Project + if c.ArchivedProjectHandling == ArchivedProjectIgnore && project.Archived { + return nil + } p := Project{ ID: project.ID, - Name: project.Path, + Path: project.Path, DefaultBranch: project.DefaultBranch, } if p.DefaultBranch == "" { @@ -38,5 +43,8 @@ func (c *gitlabClient) newProjectFromGitlabProject(project *gitlab.Project) Proj } else { p.CloneURL = project.HTTPURLToRepo } - return p + if c.ArchivedProjectHandling == ArchivedProjectHide && project.Archived { + p.Path = path.Join(path.Dir(p.Path), "."+path.Base(p.Path)) + } + return &p } diff --git a/platforms/gitlab/user.go b/platforms/gitlab/user.go index 75acd9d..3ce4d9d 100644 --- a/platforms/gitlab/user.go +++ b/platforms/gitlab/user.go @@ -105,7 +105,9 @@ func (c *gitlabClient) fetchUserContent(user *User) (map[string]fstree.GroupSour } for _, gitlabProject := range gitlabProjects { project := c.newProjectFromGitlabProject(gitlabProject) - childProjects[project.Name] = &project + if project != nil { + childProjects[project.Path] = project + } } if response.CurrentPage >= response.TotalPages { break From 36b3963ac3ae54793025119088336c604ffbb0d9 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Fri, 19 Jul 2024 00:12:39 -0400 Subject: [PATCH 15/33] fix missing mutex lock --- platforms/gitlab/user.go | 1 + 1 file changed, 1 insertion(+) diff --git a/platforms/gitlab/user.go b/platforms/gitlab/user.go index 3ce4d9d..315c95c 100644 --- a/platforms/gitlab/user.go +++ b/platforms/gitlab/user.go @@ -57,6 +57,7 @@ func (c *gitlabClient) fetchUser(uid int) (*User, error) { } // save in cache + c.userCacheMux.Lock() c.userCache[uid] = &newUser c.userCacheMux.Unlock() From 4e2b631a0cc276d0a6a18b65a8e80ef046371977 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Sun, 4 Aug 2024 15:44:50 -0400 Subject: [PATCH 16/33] refactor config loader and add github config --- config.example.yaml | 37 +++++++++- config/config.test.yaml | 18 ++++- config/loader.go | 112 +++++++++++++++++++++++++------ config/loader_test.go | 49 +++++++++----- git/client.go | 17 ++--- git/clone.go | 14 ++-- git/pull.go | 8 +-- go.mod | 1 + go.sum | 2 + main.go | 26 +++++-- platforms/github/client.go | 42 ++++++++++++ platforms/github/organization.go | 1 + platforms/github/user.go | 1 + platforms/gitlab/client.go | 24 +------ platforms/gitlab/project.go | 7 +- 15 files changed, 263 insertions(+), 96 deletions(-) create mode 100644 platforms/github/client.go create mode 100644 platforms/github/organization.go create mode 100644 platforms/github/user.go diff --git a/config.example.yaml b/config.example.yaml index f80d607..ac13455 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -6,6 +6,10 @@ fs: # See mount.fuse(8) for the full list of options. #mountoptions: nodev,nosuid + # The git platform to use as the backend. + # Must be one of "gitlab", or "github" + platform: gitlab + gitlab: # The gitlab url. url: https://gitlab.com @@ -16,7 +20,7 @@ gitlab: # Must be set to either "http" or "ssh". # The protocol to configure the git remote on. - # "http" may not work on private repos unless a credential manager is configured + # "http" may not work on private projects unless a credential manager is configured # If possible, prefer "ssh" over "http" pull_method: http @@ -28,7 +32,7 @@ gitlab: user_ids: [] # Set how archived projects are handled. - # If set to "show", it will add them to the filesystem and treat them like any other repository + # If set to "show", it will add them to the filesystem and treat them like any other project # If set to "hide", it will add them to the filesystem, but prefix the symlink with a "." # If set to "ignore", it will make them absent from the filesystem # Default to "hide" @@ -37,6 +41,35 @@ gitlab: # If set to true, the user the api token belongs to will automatically be added to the list of users exposed by the filesystem. include_current_user: true +github: + # The github api token + # Default to anonymous (only public repositories will be visible) + #token: + + # Must be set to either "http" or "ssh". + # The protocol to configure the git remote on. + # "http" may not work on private repositories unless a credential manager is configured + # If possible, prefer "ssh" over "http" + pull_method: http + + # A list of the name of the organizations to expose in the filesystem + org_names: [] + + # A list of the name of the user to expose their repositories un the filesystem + user_names: [] + + # Set how archived repositories are handled. + # If set to "show", it will add them to the filesystem and treat them like any other repository + # If set to "hide", it will add them to the filesystem, but prefix the symlink with a "." + # If set to "ignore", it will make them absent from the filesystem + # Default to "hide" + archived_repo_handling: hide + + # If set to true, the personal repositories and the repositories of the organizations the user the api token belongs to + # will be automatically be added to the list of users exposed by the filesystem. + include_current_user: true + + git: # Path to the local repository cache. Repositories in the filesystem will symlink to a folder in this path. # Default to $XDG_DATA_HOME/gitlabfs, or $HOME/.local/share/gitlabfs if the environment variable $XDG_DATA_HOME is unset. diff --git a/config/config.test.yaml b/config/config.test.yaml index e8a0f3d..5d21107 100644 --- a/config/config.test.yaml +++ b/config/config.test.yaml @@ -1,19 +1,31 @@ fs: - mountpoint: /tmp/gitlabfs/test/mnt + mountpoint: /tmp/gitlabfs/test/mnt/gitlab mountoptions: nodev + platform: gitlab gitlab: url: https://example.com token: "12345" + pull_method: ssh group_ids: - 123 user_ids: - 456 + archived_project_handling: hide + include_current_user: true + +github: + token: "12345" + pull_method: http + org_names: + - test-org + user_names: + - test-user + archived_repo_handling: hide include_current_user: true - pull_method: ssh git: - clone_location: /tmp/gitlabfs/test/clone + clone_location: /tmp/gitlabfs/test/cache/gitlab remote: origin on_clone: clone auto_pull: false diff --git a/config/loader.go b/config/loader.go index b0d5b5d..f20f428 100644 --- a/config/loader.go +++ b/config/loader.go @@ -5,20 +5,62 @@ import ( "os" "path/filepath" - "github.com/badjware/gitlabfs/git" - "github.com/badjware/gitlabfs/platforms/gitlab" "gopkg.in/yaml.v2" ) +const ( + PlatformGitlab = "gitlab" + PlatformGithub = "github" + + PullMethodHTTP = "http" + PullMethodSSH = "ssh" + + ArchivedProjectShow = "show" + ArchivedProjectHide = "hide" + ArchivedProjectIgnore = "ignore" +) + type ( Config struct { - FS FSConfig `yaml:"fs,omitempty"` - Gitlab gitlab.GitlabClientConfig `yaml:"gitlab,omitempty"` - Git git.GitClientParam `yaml:"git,omitempty"` + FS FSConfig `yaml:"fs,omitempty"` + Gitlab GitlabClientConfig `yaml:"gitlab,omitempty"` + Github GithubClientConfig `yaml:"github,omitempty"` + Git GitClientConfig `yaml:"git,omitempty"` } FSConfig struct { Mountpoint string `yaml:"mountpoint,omitempty"` MountOptions string `yaml:"mountoptions,omitempty"` + Platform string `yaml:"platform,omitempty"` + } + GitlabClientConfig struct { + URL string `yaml:"url,omitempty"` + Token string `yaml:"token,omitempty"` + + GroupIDs []int `yaml:"group_ids,omitempty"` + UserIDs []int `yaml:"user_ids,omitempty"` + + ArchivedProjectHandling string `yaml:"archived_project_handling,omitempty"` + IncludeCurrentUser bool `yaml:"include_current_user,omitempty"` + PullMethod string `yaml:"pull_method,omitempty"` + } + GithubClientConfig struct { + Token string `yaml:"token,omitempty"` + + OrgNames []string `yaml:"org_names,omitempty"` + UserNames []string `yaml:"user_names,omitempty"` + + ArchivedRepoHandling string `yaml:"archived_repo_handling,omitempty"` + IncludeCurrentUser bool `yaml:"include_current_user,omitempty"` + PullMethod string `yaml:"pull_method,omitempty"` + } + GitClientConfig struct { + CloneLocation string `yaml:"clone_location,omitempty"` + Remote string `yaml:"remote,omitempty"` + OnClone string `yaml:"on_clone,omitempty"` + AutoPull bool `yaml:"auto_pull,omitempty"` + Depth int `yaml:"depth,omitempty"` + QueueSize int `yaml:"queue_size,omitempty"` + QueueWorkerCount int `yaml:"worker_count,omitempty"` } ) @@ -34,8 +76,9 @@ func LoadConfig(configPath string) (*Config, error) { FS: FSConfig{ Mountpoint: "", MountOptions: "nodev,nosuid", + Platform: "", }, - Gitlab: gitlab.GitlabClientConfig{ + Gitlab: GitlabClientConfig{ URL: "https://gitlab.com", Token: "", PullMethod: "http", @@ -44,7 +87,15 @@ func LoadConfig(configPath string) (*Config, error) { ArchivedProjectHandling: "hide", IncludeCurrentUser: true, }, - Git: git.GitClientParam{ + Github: GithubClientConfig{ + Token: "", + PullMethod: "http", + OrgNames: []string{}, + UserNames: []string{}, + ArchivedRepoHandling: "hide", + IncludeCurrentUser: true, + }, + Git: GitClientConfig{ CloneLocation: defaultCloneLocation, Remote: "origin", OnClone: "init", @@ -68,28 +119,47 @@ func LoadConfig(configPath string) (*Config, error) { } } + // validate platform is set + if config.FS.Platform != PlatformGithub && config.FS.Platform != PlatformGitlab { + return nil, fmt.Errorf("fs.platform must be either \"%v\", or \"%v\"", PlatformGitlab, PlatformGithub) + } + return config, nil } -func MakeGitConfig(config *Config) (*git.GitClientParam, error) { - // parse on_clone - if config.Git.OnClone != "init" && config.Git.OnClone != "clone" { - return nil, fmt.Errorf("on_clone must be either \"init\" or \"clone\"") - } - - return &config.Git, nil -} - -func MakeGitlabConfig(config *Config) (*gitlab.GitlabClientConfig, error) { +func MakeGitlabConfig(config *Config) (*GitlabClientConfig, error) { // parse pull_method - if config.Gitlab.PullMethod != gitlab.PullMethodHTTP && config.Gitlab.PullMethod != gitlab.PullMethodSSH { - return nil, fmt.Errorf("pull_method must be either \"%v\" or \"%v\"", gitlab.PullMethodHTTP, gitlab.PullMethodSSH) + if config.Gitlab.PullMethod != PullMethodHTTP && config.Gitlab.PullMethod != PullMethodSSH { + return nil, fmt.Errorf("gitlab.pull_method must be either \"%v\" or \"%v\"", PullMethodHTTP, PullMethodSSH) } // parse archive_handing - if config.Gitlab.ArchivedProjectHandling != gitlab.ArchivedProjectShow && config.Gitlab.ArchivedProjectHandling != gitlab.ArchivedProjectHide && config.Gitlab.ArchivedProjectHandling != gitlab.ArchivedProjectIgnore { - return nil, fmt.Errorf("pull_method must be either \"%v\", \"%v\" or \"%v\"", gitlab.ArchivedProjectShow, gitlab.ArchivedProjectHide, gitlab.ArchivedProjectIgnore) + if config.Gitlab.ArchivedProjectHandling != ArchivedProjectShow && config.Gitlab.ArchivedProjectHandling != ArchivedProjectHide && config.Gitlab.ArchivedProjectHandling != ArchivedProjectIgnore { + return nil, fmt.Errorf("gitlab.archived_project_handling must be either \"%v\", \"%v\" or \"%v\"", ArchivedProjectShow, ArchivedProjectHide, ArchivedProjectIgnore) } return &config.Gitlab, nil } + +func MakeGithubConfig(config *Config) (*GithubClientConfig, error) { + // parse pull_method + if config.Gitlab.PullMethod != PullMethodHTTP && config.Gitlab.PullMethod != PullMethodSSH { + return nil, fmt.Errorf("github.pull_method must be either \"%v\" or \"%v\"", PullMethodHTTP, PullMethodSSH) + } + + // parse archive_handing + if config.Gitlab.ArchivedProjectHandling != ArchivedProjectShow && config.Gitlab.ArchivedProjectHandling != ArchivedProjectHide && config.Gitlab.ArchivedProjectHandling != ArchivedProjectIgnore { + return nil, fmt.Errorf("github.archived_repo_handling must be either \"%v\", \"%v\" or \"%v\"", ArchivedProjectShow, ArchivedProjectHide, ArchivedProjectIgnore) + } + + return &config.Github, nil +} + +func MakeGitConfig(config *Config) (*GitClientConfig, error) { + // parse on_clone + if config.Git.OnClone != "init" && config.Git.OnClone != "clone" { + return nil, fmt.Errorf("git.on_clone must be either \"init\" or \"clone\"") + } + + return &config.Git, nil +} diff --git a/config/loader_test.go b/config/loader_test.go index 0d6bf95..8b0a072 100644 --- a/config/loader_test.go +++ b/config/loader_test.go @@ -5,8 +5,6 @@ import ( "testing" "github.com/badjware/gitlabfs/config" - "github.com/badjware/gitlabfs/git" - "github.com/badjware/gitlabfs/platforms/gitlab" ) func TestLoadConfig(t *testing.T) { @@ -18,10 +16,11 @@ func TestLoadConfig(t *testing.T) { input: "config.test.yaml", expected: &config.Config{ FS: config.FSConfig{ - Mountpoint: "/tmp/gitlabfs/test/mnt", + Mountpoint: "/tmp/gitlabfs/test/mnt/gitlab", MountOptions: "nodev", + Platform: "gitlab", }, - Gitlab: gitlab.GitlabClientConfig{ + Gitlab: config.GitlabClientConfig{ URL: "https://example.com", Token: "12345", PullMethod: "ssh", @@ -30,8 +29,16 @@ func TestLoadConfig(t *testing.T) { ArchivedProjectHandling: "hide", IncludeCurrentUser: true, }, - Git: git.GitClientParam{ - CloneLocation: "/tmp/gitlabfs/test/clone", + Github: config.GithubClientConfig{ + Token: "12345", + PullMethod: "http", + OrgNames: []string{"test-org"}, + UserNames: []string{"test-user"}, + ArchivedRepoHandling: "hide", + IncludeCurrentUser: true, + }, + Git: config.GitClientConfig{ + CloneLocation: "/tmp/gitlabfs/test/cache/gitlab", Remote: "origin", OnClone: "clone", AutoPull: false, @@ -58,11 +65,14 @@ func TestLoadConfig(t *testing.T) { func TestMakeGitConfig(t *testing.T) { tests := map[string]struct { input *config.Config - expected *git.GitClientParam + expected *config.GitClientConfig }{ "ValidConfig": { input: &config.Config{ - Git: git.GitClientParam{ + FS: config.FSConfig{ + Platform: "gitlab", + }, + Git: config.GitClientConfig{ CloneLocation: "/tmp", Remote: "origin", OnClone: "init", @@ -72,7 +82,7 @@ func TestMakeGitConfig(t *testing.T) { QueueWorkerCount: 5, }, }, - expected: &git.GitClientParam{ + expected: &config.GitClientConfig{ CloneLocation: "/tmp", Remote: "origin", OnClone: "init", @@ -84,7 +94,10 @@ func TestMakeGitConfig(t *testing.T) { }, "InvalidOnClone": { input: &config.Config{ - Git: git.GitClientParam{ + FS: config.FSConfig{ + Platform: "gitlab", + }, + Git: config.GitClientConfig{ CloneLocation: "/tmp", Remote: "origin", OnClone: "invalid", @@ -114,11 +127,14 @@ func TestMakeGitConfig(t *testing.T) { func TestMakeGitlabConfig(t *testing.T) { tests := map[string]struct { input *config.Config - expected *gitlab.GitlabClientConfig + expected *config.GitlabClientConfig }{ "ValidConfig": { input: &config.Config{ - Gitlab: gitlab.GitlabClientConfig{ + FS: config.FSConfig{ + Platform: "gitlab", + }, + Gitlab: config.GitlabClientConfig{ URL: "https://gitlab.com", PullMethod: "http", Token: "", @@ -128,7 +144,7 @@ func TestMakeGitlabConfig(t *testing.T) { IncludeCurrentUser: true, }, }, - expected: &gitlab.GitlabClientConfig{ + expected: &config.GitlabClientConfig{ URL: "https://gitlab.com", PullMethod: "http", Token: "", @@ -140,7 +156,10 @@ func TestMakeGitlabConfig(t *testing.T) { }, "InvalidPullMethod": { input: &config.Config{ - Gitlab: gitlab.GitlabClientConfig{ + FS: config.FSConfig{ + Platform: "gitlab", + }, + Gitlab: config.GitlabClientConfig{ URL: "https://gitlab.com", PullMethod: "invalid", Token: "", @@ -154,7 +173,7 @@ func TestMakeGitlabConfig(t *testing.T) { }, "InvalidArchiveHandling": { input: &config.Config{ - Gitlab: gitlab.GitlabClientConfig{ + Gitlab: config.GitlabClientConfig{ URL: "https://gitlab.com", PullMethod: "http", Token: "", diff --git a/git/client.go b/git/client.go index da90295..e89a4bd 100644 --- a/git/client.go +++ b/git/client.go @@ -10,24 +10,15 @@ import ( "strconv" "time" + "github.com/badjware/gitlabfs/config" "github.com/badjware/gitlabfs/fstree" "github.com/badjware/gitlabfs/utils" "github.com/vmihailenco/taskq/v3" "github.com/vmihailenco/taskq/v3/memqueue" ) -type GitClientParam struct { - CloneLocation string `yaml:"clone_location,omitempty"` - Remote string `yaml:"remote,omitempty"` - OnClone string `yaml:"on_clone,omitempty"` - AutoPull bool `yaml:"auto_pull,omitempty"` - Depth int `yaml:"depth,omitempty"` - QueueSize int `yaml:"queue_size,omitempty"` - QueueWorkerCount int `yaml:"worker_count,omitempty"` -} - type gitClient struct { - GitClientParam + config.GitClientConfig logger *slog.Logger @@ -42,11 +33,11 @@ type gitClient struct { pullTask *taskq.Task } -func NewClient(logger *slog.Logger, p GitClientParam) (*gitClient, error) { +func NewClient(logger *slog.Logger, p config.GitClientConfig) (*gitClient, error) { queueFactory := memqueue.NewFactory() // Create the client c := &gitClient{ - GitClientParam: p, + GitClientConfig: p, logger: logger, diff --git a/git/clone.go b/git/clone.go index 3fbcb47..eec9c78 100644 --- a/git/clone.go +++ b/git/clone.go @@ -8,7 +8,7 @@ import ( ) func (c *gitClient) clone(url string, defaultBranch string, dst string) error { - if c.GitClientParam.OnClone == "init" { + if c.GitClientConfig.OnClone == "init" { // "Fake" cloning the repo by never actually talking to the git server // This skip a fetch operation that we would do if we where to do a proper clone // We can save a lot of time and network i/o doing it this way, at the cost of @@ -40,8 +40,8 @@ func (c *gitClient) clone(url string, defaultBranch string, dst string) error { "git", "remote", "add", "-m", defaultBranch, "--", - c.GitClientParam.Remote, // name - url, // url + c.GitClientConfig.Remote, // name + url, // url ) if err != nil { return fmt.Errorf("failed to setup remote %v in git repo %v: %v", url, dst, err) @@ -54,7 +54,7 @@ func (c *gitClient) clone(url string, defaultBranch string, dst string) error { "git", "config", "--local", "--", fmt.Sprintf("branch.%s.remote", defaultBranch), // key - c.GitClientParam.Remote, // value + c.GitClientConfig.Remote, // value ) if err != nil { @@ -77,10 +77,10 @@ func (c *gitClient) clone(url string, defaultBranch string, dst string) error { c.logger.Info("Cloning git repository", "directory", dst, "repository", url) args := []string{ "clone", - "--origin", c.GitClientParam.Remote, + "--origin", c.GitClientConfig.Remote, } - if c.GitClientParam.Depth != 0 { - args = append(args, "--depth", strconv.Itoa(c.GitClientParam.Depth)) + if c.GitClientConfig.Depth != 0 { + args = append(args, "--depth", strconv.Itoa(c.GitClientConfig.Depth)) } args = append(args, "--", diff --git a/git/pull.go b/git/pull.go index ef5f9dd..ed5a448 100644 --- a/git/pull.go +++ b/git/pull.go @@ -24,13 +24,13 @@ func (c *gitClient) pull(repoPath string, defaultBranch string) error { args := []string{ "pull", } - if c.GitClientParam.Depth != 0 { - args = append(args, "--depth", strconv.Itoa(c.GitClientParam.Depth)) + if c.GitClientConfig.Depth != 0 { + args = append(args, "--depth", strconv.Itoa(c.GitClientConfig.Depth)) } args = append(args, "--", - c.GitClientParam.Remote, // repository - defaultBranch, // refspec + c.GitClientConfig.Remote, // repository + defaultBranch, // refspec ) _, err = utils.ExecProcessInDir(c.logger, repoPath, "git", args...) diff --git a/go.mod b/go.mod index 100b39d..358f256 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/go-redis/redis/v8 v8.11.4 // indirect github.com/go-redis/redis_rate/v9 v9.1.2 // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/google/go-github/v63 v63.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.6.8 // indirect diff --git a/go.sum b/go.sum index 6dccd6e..b3a093a 100644 --- a/go.sum +++ b/go.sum @@ -114,6 +114,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github/v63 v63.0.0 h1:13xwK/wk9alSokujB9lJkuzdmQuVn2QCPeck76wR3nE= +github.com/google/go-github/v63 v63.0.0/go.mod h1:IqbcrgUmIcEaioWrGYei/09o+ge5vhffGOcxrO0AfmA= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= diff --git a/main.go b/main.go index 6cb9626..3a0d421 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( "github.com/badjware/gitlabfs/config" "github.com/badjware/gitlabfs/fstree" "github.com/badjware/gitlabfs/git" + "github.com/badjware/gitlabfs/platforms/github" "github.com/badjware/gitlabfs/platforms/gitlab" ) @@ -64,20 +65,31 @@ func main() { } gitClient, _ := git.NewClient(logger, *gitClientParam) - // Create the gitlab client - GitlabClientConfig, err := config.MakeGitlabConfig(loadedConfig) - if err != nil { - fmt.Println(err) - os.Exit(1) + var gitPlatformClient fstree.GitPlatform + if loadedConfig.FS.Platform == config.PlatformGitlab { + // Create the gitlab client + GitlabClientConfig, err := config.MakeGitlabConfig(loadedConfig) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + gitPlatformClient, _ = gitlab.NewClient(logger, loadedConfig.Gitlab.URL, loadedConfig.Gitlab.Token, *GitlabClientConfig) + } else if loadedConfig.FS.Platform == config.PlatformGithub { + // Create the github client + GithubClientConfig, err := config.MakeGithubConfig(loadedConfig) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + gitPlatformClient, _ = github.NewClient(logger, *GithubClientConfig) } - gitlabClient, _ := gitlab.NewClient(logger, loadedConfig.Gitlab.URL, loadedConfig.Gitlab.Token, *GitlabClientConfig) // Start the filesystem err = fstree.Start( logger, mountpoint, parsedMountoptions, - &fstree.FSParam{GitClient: gitClient, GitPlatform: gitlabClient}, + &fstree.FSParam{GitClient: gitClient, GitPlatform: gitPlatformClient}, *debug, ) if err != nil { diff --git a/platforms/github/client.go b/platforms/github/client.go new file mode 100644 index 0000000..7a3624c --- /dev/null +++ b/platforms/github/client.go @@ -0,0 +1,42 @@ +package github + +import ( + "log/slog" + + "github.com/badjware/gitlabfs/config" + "github.com/badjware/gitlabfs/fstree" + "github.com/google/go-github/v63/github" +) + +type githubClient struct { + config.GithubClientConfig + client *github.Client + + logger *slog.Logger + + rootContent map[string]fstree.GroupSource +} + +func NewClient(logger *slog.Logger, config config.GithubClientConfig) (*githubClient, error) { + client := github.NewClient(nil) + if config.Token != "" { + client = client.WithAuthToken(config.Token) + } + + gitHubClient := &githubClient{ + GithubClientConfig: config, + client: client, + + logger: logger, + } + + return gitHubClient, nil +} + +func (c *githubClient) FetchRootGroupContent() (map[string]fstree.GroupSource, error) { + return nil, nil +} + +func (c *githubClient) FetchGroupContent(gid uint64) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) { + return nil, nil, nil +} diff --git a/platforms/github/organization.go b/platforms/github/organization.go new file mode 100644 index 0000000..d2e73c2 --- /dev/null +++ b/platforms/github/organization.go @@ -0,0 +1 @@ +package github diff --git a/platforms/github/user.go b/platforms/github/user.go new file mode 100644 index 0000000..d2e73c2 --- /dev/null +++ b/platforms/github/user.go @@ -0,0 +1 @@ +package github diff --git a/platforms/gitlab/client.go b/platforms/gitlab/client.go index d7d99f0..44e2000 100644 --- a/platforms/gitlab/client.go +++ b/platforms/gitlab/client.go @@ -6,31 +6,13 @@ import ( "slices" "sync" + "github.com/badjware/gitlabfs/config" "github.com/badjware/gitlabfs/fstree" "github.com/xanzy/go-gitlab" ) -const ( - PullMethodHTTP = "http" - PullMethodSSH = "ssh" - - ArchivedProjectShow = "show" - ArchivedProjectHide = "hide" - ArchivedProjectIgnore = "ignore" -) - -type GitlabClientConfig struct { - URL string `yaml:"url,omitempty"` - Token string `yaml:"token,omitempty"` - GroupIDs []int `yaml:"group_ids,omitempty"` - UserIDs []int `yaml:"user_ids,omitempty"` - ArchivedProjectHandling string `yaml:"archived_project_handling,omitempty"` - IncludeCurrentUser bool `yaml:"include_current_user,omitempty"` - PullMethod string `yaml:"pull_method,omitempty"` -} - type gitlabClient struct { - GitlabClientConfig + config.GitlabClientConfig client *gitlab.Client logger *slog.Logger @@ -46,7 +28,7 @@ type gitlabClient struct { userCache map[int]*User } -func NewClient(logger *slog.Logger, gitlabUrl string, gitlabToken string, p GitlabClientConfig) (*gitlabClient, error) { +func NewClient(logger *slog.Logger, gitlabUrl string, gitlabToken string, p config.GitlabClientConfig) (*gitlabClient, error) { client, err := gitlab.NewClient( gitlabToken, gitlab.WithBaseURL(gitlabUrl), diff --git a/platforms/gitlab/project.go b/platforms/gitlab/project.go index af366a2..d0abc65 100644 --- a/platforms/gitlab/project.go +++ b/platforms/gitlab/project.go @@ -3,6 +3,7 @@ package gitlab import ( "path" + "github.com/badjware/gitlabfs/config" "github.com/xanzy/go-gitlab" ) @@ -27,7 +28,7 @@ func (p *Project) GetDefaultBranch() string { func (c *gitlabClient) newProjectFromGitlabProject(project *gitlab.Project) *Project { // https://godoc.org/github.com/xanzy/go-gitlab#Project - if c.ArchivedProjectHandling == ArchivedProjectIgnore && project.Archived { + if c.ArchivedProjectHandling == config.ArchivedProjectIgnore && project.Archived { return nil } p := Project{ @@ -38,12 +39,12 @@ func (c *gitlabClient) newProjectFromGitlabProject(project *gitlab.Project) *Pro if p.DefaultBranch == "" { p.DefaultBranch = "master" } - if c.PullMethod == PullMethodSSH { + if c.PullMethod == config.PullMethodSSH { p.CloneURL = project.SSHURLToRepo } else { p.CloneURL = project.HTTPURLToRepo } - if c.ArchivedProjectHandling == ArchivedProjectHide && project.Archived { + if c.ArchivedProjectHandling == config.ArchivedProjectHide && project.Archived { p.Path = path.Join(path.Dir(p.Path), "."+path.Base(p.Path)) } return &p From a0aaa4491bcfcbb7bf3f1c45cf05d13589e3c588 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Sun, 4 Aug 2024 18:59:57 -0400 Subject: [PATCH 17/33] add support for github orgs --- fstree/group.go | 57 +++++++++-------- main.go | 2 +- platforms/github/client.go | 34 +++++++++- platforms/github/organization.go | 103 +++++++++++++++++++++++++++++++ platforms/github/repository.go | 50 +++++++++++++++ platforms/gitlab/client.go | 8 +-- 6 files changed, 220 insertions(+), 34 deletions(-) create mode 100644 platforms/github/repository.go diff --git a/fstree/group.go b/fstree/group.go index a60337c..9f0ade0 100644 --- a/fstree/group.go +++ b/fstree/group.go @@ -70,38 +70,41 @@ func (n *groupNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { } func (n *groupNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { - groups, repositories, _ := n.param.GitPlatform.FetchGroupContent(n.source.GetGroupID()) - - // Check if the map of groups contains it - group, found := groups[name] - if found { - attrs := fs.StableAttr{ - Ino: group.GetGroupID(), - Mode: fuse.S_IFDIR, + groups, repositories, err := n.param.GitPlatform.FetchGroupContent(n.source.GetGroupID()) + if err != nil { + n.param.logger.Error(err.Error()) + } else { + // Check if the map of groups contains it + group, found := groups[name] + if found { + attrs := fs.StableAttr{ + Ino: group.GetGroupID(), + Mode: fuse.S_IFDIR, + } + groupNode, _ := newGroupNodeFromSource(group, n.param) + return n.NewInode(ctx, groupNode, attrs), 0 } - groupNode, _ := newGroupNodeFromSource(group, n.param) - return n.NewInode(ctx, groupNode, attrs), 0 - } - // Check if the map of projects contains it - repository, found := repositories[name] - if found { - attrs := fs.StableAttr{ - Ino: repository.GetRepositoryID(), - Mode: fuse.S_IFLNK, + // Check if the map of projects contains it + repository, found := repositories[name] + if found { + attrs := fs.StableAttr{ + Ino: repository.GetRepositoryID(), + Mode: fuse.S_IFLNK, + } + repositoryNode, _ := newRepositoryNodeFromSource(repository, n.param) + return n.NewInode(ctx, repositoryNode, attrs), 0 } - repositoryNode, _ := newRepositoryNodeFromSource(repository, n.param) - return n.NewInode(ctx, repositoryNode, attrs), 0 - } - // Check if the map of static nodes contains it - staticNode, ok := n.staticNodes[name] - if ok { - attrs := fs.StableAttr{ - Ino: staticNode.Ino(), - Mode: staticNode.Mode(), + // Check if the map of static nodes contains it + staticNode, ok := n.staticNodes[name] + if ok { + attrs := fs.StableAttr{ + Ino: staticNode.Ino(), + Mode: staticNode.Mode(), + } + return n.NewInode(ctx, staticNode, attrs), 0 } - return n.NewInode(ctx, staticNode, attrs), 0 } return nil, syscall.ENOENT diff --git a/main.go b/main.go index 3a0d421..0af7f52 100644 --- a/main.go +++ b/main.go @@ -73,7 +73,7 @@ func main() { fmt.Println(err) os.Exit(1) } - gitPlatformClient, _ = gitlab.NewClient(logger, loadedConfig.Gitlab.URL, loadedConfig.Gitlab.Token, *GitlabClientConfig) + gitPlatformClient, _ = gitlab.NewClient(logger, *GitlabClientConfig) } else if loadedConfig.FS.Platform == config.PlatformGithub { // Create the github client GithubClientConfig, err := config.MakeGithubConfig(loadedConfig) diff --git a/platforms/github/client.go b/platforms/github/client.go index 7a3624c..8381519 100644 --- a/platforms/github/client.go +++ b/platforms/github/client.go @@ -1,7 +1,9 @@ package github import ( + "fmt" "log/slog" + "sync" "github.com/badjware/gitlabfs/config" "github.com/badjware/gitlabfs/fstree" @@ -15,6 +17,11 @@ type githubClient struct { logger *slog.Logger rootContent map[string]fstree.GroupSource + + // API response cache + organizationCacheMux sync.RWMutex + organizationNameToIDMap map[string]int64 + organizationCache map[int64]*Organization } func NewClient(logger *slog.Logger, config config.GithubClientConfig) (*githubClient, error) { @@ -28,15 +35,38 @@ func NewClient(logger *slog.Logger, config config.GithubClientConfig) (*githubCl client: client, logger: logger, + + rootContent: nil, + + organizationNameToIDMap: map[string]int64{}, + organizationCache: map[int64]*Organization{}, } return gitHubClient, nil } func (c *githubClient) FetchRootGroupContent() (map[string]fstree.GroupSource, error) { - return nil, nil + if c.rootContent == nil { + rootContent := make(map[string]fstree.GroupSource) + + for _, org_name := range c.GithubClientConfig.OrgNames { + org, err := c.fetchOrganization(org_name) + if err != nil { + c.logger.Warn(err.Error()) + } else { + rootContent[org.Name] = org + } + } + // TODO: user + current user + + c.rootContent = rootContent + } + return c.rootContent, nil } func (c *githubClient) FetchGroupContent(gid uint64) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) { - return nil, nil, nil + if org, found := c.organizationCache[int64(gid)]; found { + return c.fetchOrganizationContent(org) + } + return nil, nil, fmt.Errorf("invalid gid: %v", gid) } diff --git a/platforms/github/organization.go b/platforms/github/organization.go index d2e73c2..ffee699 100644 --- a/platforms/github/organization.go +++ b/platforms/github/organization.go @@ -1 +1,104 @@ package github + +import ( + "context" + "fmt" + "sync" + + "github.com/badjware/gitlabfs/fstree" + "github.com/google/go-github/v63/github" +) + +type Organization struct { + ID int64 + Name string + + mux sync.Mutex + + // hold org content + childRepositories map[string]fstree.RepositorySource +} + +func (o *Organization) GetGroupID() uint64 { + return uint64(o.ID) +} + +func (o *Organization) InvalidateContentCache() { + o.mux.Lock() + defer o.mux.Unlock() + + // clear child repositories from cache + o.childRepositories = nil +} + +func (c *githubClient) fetchOrganization(org_name string) (*Organization, error) { + c.organizationCacheMux.RLock() + cachedId, found := c.organizationNameToIDMap[org_name] + if found { + cachedOrg := c.organizationCache[cachedId] + c.organizationCacheMux.RUnlock() + + // if found in cache, return the cached reference + c.logger.Debug("Organization cache hit", "org_name", org_name) + return cachedOrg, nil + } else { + c.organizationCacheMux.RUnlock() + + c.logger.Debug("Organization cache miss", "org_name", org_name) + } + + // If not found in cache, fetch organization infos from API + githubOrg, _, err := c.client.Organizations.Get(context.Background(), org_name) + if err != nil { + return nil, fmt.Errorf("failed to fetch organization with name %v: %v", org_name, err) + } + newOrg := Organization{ + ID: *githubOrg.ID, + Name: *githubOrg.Login, + + childRepositories: nil, + } + + // save in cache + c.organizationCacheMux.Lock() + c.organizationCache[newOrg.ID] = &newOrg + c.organizationCacheMux.Unlock() + + return &newOrg, nil +} + +func (c *githubClient) fetchOrganizationContent(org *Organization) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) { + org.mux.Lock() + defer org.mux.Unlock() + + // Get cached data if available + // TODO: cache cache invalidation? + if org.childRepositories == nil { + childRepositories := make(map[string]fstree.RepositorySource) + + // Fetch the organization repositories + repositoryListOpt := &github.RepositoryListByOrgOptions{ + ListOptions: github.ListOptions{PerPage: 100}, + } + for { + githubRepositories, response, err := c.client.Repositories.ListByOrg(context.Background(), org.Name, repositoryListOpt) + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch repository in github: %v", err) + } + for _, githubRepository := range githubRepositories { + repository := c.newRepositoryFromGithubRepository(githubRepository) + if repository != nil { + childRepositories[repository.Path] = repository + } + } + if response.NextPage == 0 { + break + } + // Get the next page + repositoryListOpt.Page = response.NextPage + } + + org.childRepositories = childRepositories + } + return make(map[string]fstree.GroupSource), org.childRepositories, nil +} diff --git a/platforms/github/repository.go b/platforms/github/repository.go new file mode 100644 index 0000000..abbf482 --- /dev/null +++ b/platforms/github/repository.go @@ -0,0 +1,50 @@ +package github + +import ( + "path" + + "github.com/badjware/gitlabfs/config" + "github.com/google/go-github/v63/github" +) + +type Repository struct { + ID int64 + Path string + CloneURL string + DefaultBranch string +} + +func (r *Repository) GetRepositoryID() uint64 { + return uint64(r.ID) +} + +func (r *Repository) GetCloneURL() string { + return r.CloneURL +} + +func (r *Repository) GetDefaultBranch() string { + return r.DefaultBranch +} + +func (c *githubClient) newRepositoryFromGithubRepository(repository *github.Repository) *Repository { + if c.ArchivedRepoHandling == config.ArchivedProjectIgnore && *repository.Archived { + return nil + } + r := Repository{ + ID: *repository.ID, + Path: *repository.Name, + DefaultBranch: *repository.DefaultBranch, + } + if r.DefaultBranch == "" { + r.DefaultBranch = "master" + } + if c.PullMethod == config.PullMethodSSH { + r.CloneURL = *repository.SSHURL + } else { + r.CloneURL = *repository.CloneURL + } + if c.ArchivedRepoHandling == config.ArchivedProjectHide && *repository.Archived { + r.Path = path.Join(path.Dir(r.Path), "."+path.Base(r.Path)) + } + return &r +} diff --git a/platforms/gitlab/client.go b/platforms/gitlab/client.go index 44e2000..080744a 100644 --- a/platforms/gitlab/client.go +++ b/platforms/gitlab/client.go @@ -28,17 +28,17 @@ type gitlabClient struct { userCache map[int]*User } -func NewClient(logger *slog.Logger, gitlabUrl string, gitlabToken string, p config.GitlabClientConfig) (*gitlabClient, error) { +func NewClient(logger *slog.Logger, config config.GitlabClientConfig) (*gitlabClient, error) { client, err := gitlab.NewClient( - gitlabToken, - gitlab.WithBaseURL(gitlabUrl), + config.Token, + gitlab.WithBaseURL(config.URL), ) if err != nil { return nil, fmt.Errorf("failed to create gitlab client: %v", err) } gitlabClient := &gitlabClient{ - GitlabClientConfig: p, + GitlabClientConfig: config, client: client, logger: logger, From 937e5c341d967fe6e36c2554488cae371dbfb758 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Sun, 4 Aug 2024 21:00:34 -0400 Subject: [PATCH 18/33] add support for github users --- platforms/github/client.go | 17 +++++ platforms/github/organization.go | 1 + platforms/github/user.go | 104 +++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+) diff --git a/platforms/github/client.go b/platforms/github/client.go index 8381519..2cf4d1e 100644 --- a/platforms/github/client.go +++ b/platforms/github/client.go @@ -22,6 +22,9 @@ type githubClient struct { organizationCacheMux sync.RWMutex organizationNameToIDMap map[string]int64 organizationCache map[int64]*Organization + userCacheMux sync.RWMutex + userNameToIDMap map[string]int64 + userCache map[int64]*User } func NewClient(logger *slog.Logger, config config.GithubClientConfig) (*githubClient, error) { @@ -40,6 +43,8 @@ func NewClient(logger *slog.Logger, config config.GithubClientConfig) (*githubCl organizationNameToIDMap: map[string]int64{}, organizationCache: map[int64]*Organization{}, + userNameToIDMap: map[string]int64{}, + userCache: map[int64]*User{}, } return gitHubClient, nil @@ -57,6 +62,15 @@ func (c *githubClient) FetchRootGroupContent() (map[string]fstree.GroupSource, e rootContent[org.Name] = org } } + + for _, user_name := range c.GithubClientConfig.UserNames { + user, err := c.fetchUser(user_name) + if err != nil { + c.logger.Warn(err.Error()) + } else { + rootContent[user.Name] = user + } + } // TODO: user + current user c.rootContent = rootContent @@ -68,5 +82,8 @@ func (c *githubClient) FetchGroupContent(gid uint64) (map[string]fstree.GroupSou if org, found := c.organizationCache[int64(gid)]; found { return c.fetchOrganizationContent(org) } + if user, found := c.userCache[int64(gid)]; found { + return c.fetchUserContent(user) + } return nil, nil, fmt.Errorf("invalid gid: %v", gid) } diff --git a/platforms/github/organization.go b/platforms/github/organization.go index ffee699..49520f9 100644 --- a/platforms/github/organization.go +++ b/platforms/github/organization.go @@ -62,6 +62,7 @@ func (c *githubClient) fetchOrganization(org_name string) (*Organization, error) // save in cache c.organizationCacheMux.Lock() c.organizationCache[newOrg.ID] = &newOrg + c.organizationNameToIDMap[newOrg.Name] = newOrg.ID c.organizationCacheMux.Unlock() return &newOrg, nil diff --git a/platforms/github/user.go b/platforms/github/user.go index d2e73c2..a2ed503 100644 --- a/platforms/github/user.go +++ b/platforms/github/user.go @@ -1 +1,105 @@ package github + +import ( + "context" + "fmt" + "sync" + + "github.com/badjware/gitlabfs/fstree" + "github.com/google/go-github/v63/github" +) + +type User struct { + ID int64 + Name string + + mux sync.Mutex + + // hold user content + childRepositories map[string]fstree.RepositorySource +} + +func (u *User) GetGroupID() uint64 { + return uint64(u.ID) +} + +func (u *User) InvalidateContentCache() { + u.mux.Lock() + defer u.mux.Unlock() + + // clear child repositories from cache + u.childRepositories = nil +} + +func (c *githubClient) fetchUser(user_name string) (*User, error) { + c.userCacheMux.RLock() + cachedId, found := c.userNameToIDMap[user_name] + if found { + cachedUser := c.userCache[cachedId] + c.userCacheMux.RUnlock() + + // if found in cache, return the cached reference + c.logger.Debug("User cache hit", "user_name", user_name) + return cachedUser, nil + } else { + c.userCacheMux.RUnlock() + + c.logger.Debug("User cache miss", "user_name", user_name) + } + + // If not found in cache, fetch user infos from API + githubUser, _, err := c.client.Users.Get(context.Background(), user_name) + if err != nil { + return nil, fmt.Errorf("failed to fetch user with name %v: %v", user_name, err) + } + newUser := User{ + ID: *githubUser.ID, + Name: *githubUser.Login, + + childRepositories: nil, + } + + // save in cache + c.userCacheMux.Lock() + c.userCache[newUser.ID] = &newUser + c.userNameToIDMap[newUser.Name] = newUser.ID + c.userCacheMux.Unlock() + + return &newUser, nil +} + +func (c *githubClient) fetchUserContent(user *User) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) { + user.mux.Lock() + defer user.mux.Unlock() + + // Get cached data if available + // TODO: cache cache invalidation? + if user.childRepositories == nil { + childRepositories := make(map[string]fstree.RepositorySource) + + // Fetch the user repositories + repositoryListOpt := &github.RepositoryListByUserOptions{ + ListOptions: github.ListOptions{PerPage: 100}, + } + for { + githubRepositories, response, err := c.client.Repositories.ListByUser(context.Background(), user.Name, repositoryListOpt) + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch repository in github: %v", err) + } + for _, githubRepository := range githubRepositories { + repository := c.newRepositoryFromGithubRepository(githubRepository) + if repository != nil { + childRepositories[repository.Path] = repository + } + } + if response.NextPage == 0 { + break + } + // Get the next page + repositoryListOpt.Page = response.NextPage + } + + user.childRepositories = childRepositories + } + return make(map[string]fstree.GroupSource), user.childRepositories, nil +} From 382a0f6d8d18d6513ca1fa25d47127c9ba75d6a2 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Sun, 4 Aug 2024 21:16:03 -0400 Subject: [PATCH 19/33] add support for github current user --- platforms/github/client.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/platforms/github/client.go b/platforms/github/client.go index 2cf4d1e..e7f1285 100644 --- a/platforms/github/client.go +++ b/platforms/github/client.go @@ -1,6 +1,7 @@ package github import ( + "context" "fmt" "log/slog" "sync" @@ -47,6 +48,14 @@ func NewClient(logger *slog.Logger, config config.GithubClientConfig) (*githubCl userCache: map[int64]*User{}, } + // Fetch current user and add it to the list + currentUser, _, err := client.Users.Get(context.Background(), "") + if err != nil { + logger.Warn("failed to fetch the current user:", "error", err.Error()) + } else { + gitHubClient.UserNames = append(gitHubClient.UserNames, *currentUser.Login) + } + return gitHubClient, nil } @@ -71,7 +80,6 @@ func (c *githubClient) FetchRootGroupContent() (map[string]fstree.GroupSource, e rootContent[user.Name] = user } } - // TODO: user + current user c.rootContent = rootContent } From 0e5fed0bbd6311d0662c846acd949cce87eb1bf0 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Sun, 4 Aug 2024 21:39:15 -0400 Subject: [PATCH 20/33] refactor gitlab current user --- platforms/gitlab/client.go | 33 +++++++++++++++------------------ platforms/gitlab/user.go | 17 ----------------- 2 files changed, 15 insertions(+), 35 deletions(-) diff --git a/platforms/gitlab/client.go b/platforms/gitlab/client.go index 080744a..fd74ea7 100644 --- a/platforms/gitlab/client.go +++ b/platforms/gitlab/client.go @@ -17,9 +17,7 @@ type gitlabClient struct { logger *slog.Logger - // root group cache - rootGroupCache map[string]fstree.GroupSource - currentUserCache *User + rootContent map[string]fstree.GroupSource // API response cache groupCacheMux sync.RWMutex @@ -43,18 +41,26 @@ func NewClient(logger *slog.Logger, config config.GitlabClientConfig) (*gitlabCl logger: logger, - rootGroupCache: nil, - currentUserCache: nil, + rootContent: nil, groupCache: map[int]*Group{}, userCache: map[int]*User{}, } + + // Fetch current user and add it to the list + currentUser, _, err := client.Users.CurrentUser() + if err != nil { + logger.Warn("failed to fetch the current user:", "error", err.Error()) + } else { + gitlabClient.UserIDs = append(gitlabClient.UserIDs, currentUser.ID) + } + return gitlabClient, nil } func (c *gitlabClient) FetchRootGroupContent() (map[string]fstree.GroupSource, error) { // use cached values if available - if c.rootGroupCache == nil { + if c.rootContent == nil { rootGroupCache := make(map[string]fstree.GroupSource) // fetch root groups @@ -73,23 +79,14 @@ func (c *gitlabClient) FetchRootGroupContent() (map[string]fstree.GroupSource, e } rootGroupCache[user.Name] = user } - // fetch current user if configured - if c.IncludeCurrentUser { - currentUser, err := c.fetchCurrentUser() - if err != nil { - c.logger.Warn(err.Error()) - } else { - rootGroupCache[currentUser.Name] = currentUser - } - } - c.rootGroupCache = rootGroupCache + c.rootContent = rootGroupCache } - return c.rootGroupCache, nil + return c.rootContent, nil } func (c *gitlabClient) FetchGroupContent(gid uint64) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) { - if slices.Contains[[]int, int](c.UserIDs, int(gid)) || (c.currentUserCache != nil && c.currentUserCache.ID == int(gid)) { + if slices.Contains[[]int, int](c.UserIDs, int(gid)) { // gid is a user user, err := c.fetchUser(int(gid)) if err != nil { diff --git a/platforms/gitlab/user.go b/platforms/gitlab/user.go index 315c95c..aed80c0 100644 --- a/platforms/gitlab/user.go +++ b/platforms/gitlab/user.go @@ -64,23 +64,6 @@ func (c *gitlabClient) fetchUser(uid int) (*User, error) { return &newUser, nil } -func (c *gitlabClient) fetchCurrentUser() (*User, error) { - if c.currentUserCache == nil { - gitlabUser, _, err := c.client.Users.CurrentUser() - if err != nil { - return nil, fmt.Errorf("failed to fetch current user: %v", err) - } - newUser := User{ - ID: gitlabUser.ID, - Name: gitlabUser.Username, - - childProjects: nil, - } - c.currentUserCache = &newUser - } - return c.currentUserCache, nil -} - func (c *gitlabClient) fetchUserContent(user *User) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) { // Only a single routine can fetch the user content at the time. // We lock for the whole duration of the function to avoid fetching the same data from the API From 1a01c9ecea5714005da4199958ab59f8595662df Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Sun, 4 Aug 2024 21:45:51 -0400 Subject: [PATCH 21/33] refactor platform -> forge --- config.example.yaml | 6 +++--- config/config.test.yaml | 2 +- config/loader.go | 14 +++++++------- config/loader_test.go | 10 +++++----- {platforms => forges}/github/client.go | 0 {platforms => forges}/github/organization.go | 0 {platforms => forges}/github/repository.go | 0 {platforms => forges}/github/user.go | 0 {platforms => forges}/gitlab/client.go | 0 {platforms => forges}/gitlab/group.go | 0 {platforms => forges}/gitlab/project.go | 0 {platforms => forges}/gitlab/user.go | 0 fstree/group.go | 4 ++-- fstree/root.go | 8 ++++---- main.go | 16 ++++++++-------- 15 files changed, 30 insertions(+), 30 deletions(-) rename {platforms => forges}/github/client.go (100%) rename {platforms => forges}/github/organization.go (100%) rename {platforms => forges}/github/repository.go (100%) rename {platforms => forges}/github/user.go (100%) rename {platforms => forges}/gitlab/client.go (100%) rename {platforms => forges}/gitlab/group.go (100%) rename {platforms => forges}/gitlab/project.go (100%) rename {platforms => forges}/gitlab/user.go (100%) diff --git a/config.example.yaml b/config.example.yaml index ac13455..4dbfb1a 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -6,9 +6,9 @@ fs: # See mount.fuse(8) for the full list of options. #mountoptions: nodev,nosuid - # The git platform to use as the backend. - # Must be one of "gitlab", or "github" - platform: gitlab + # The git forge to use as the backend. + # Must be one of "gitlab" or "github" + forge: gitlab gitlab: # The gitlab url. diff --git a/config/config.test.yaml b/config/config.test.yaml index 5d21107..637c992 100644 --- a/config/config.test.yaml +++ b/config/config.test.yaml @@ -1,7 +1,7 @@ fs: mountpoint: /tmp/gitlabfs/test/mnt/gitlab mountoptions: nodev - platform: gitlab + forge: gitlab gitlab: url: https://example.com diff --git a/config/loader.go b/config/loader.go index f20f428..b785f7b 100644 --- a/config/loader.go +++ b/config/loader.go @@ -9,8 +9,8 @@ import ( ) const ( - PlatformGitlab = "gitlab" - PlatformGithub = "github" + ForgeGitlab = "gitlab" + ForgeGithub = "github" PullMethodHTTP = "http" PullMethodSSH = "ssh" @@ -30,7 +30,7 @@ type ( FSConfig struct { Mountpoint string `yaml:"mountpoint,omitempty"` MountOptions string `yaml:"mountoptions,omitempty"` - Platform string `yaml:"platform,omitempty"` + Forge string `yaml:"forge,omitempty"` } GitlabClientConfig struct { URL string `yaml:"url,omitempty"` @@ -76,7 +76,7 @@ func LoadConfig(configPath string) (*Config, error) { FS: FSConfig{ Mountpoint: "", MountOptions: "nodev,nosuid", - Platform: "", + Forge: "", }, Gitlab: GitlabClientConfig{ URL: "https://gitlab.com", @@ -119,9 +119,9 @@ func LoadConfig(configPath string) (*Config, error) { } } - // validate platform is set - if config.FS.Platform != PlatformGithub && config.FS.Platform != PlatformGitlab { - return nil, fmt.Errorf("fs.platform must be either \"%v\", or \"%v\"", PlatformGitlab, PlatformGithub) + // validate forge is set + if config.FS.Forge != ForgeGithub && config.FS.Forge != ForgeGitlab { + return nil, fmt.Errorf("fs.forge must be either \"%v\", or \"%v\"", ForgeGitlab, ForgeGithub) } return config, nil diff --git a/config/loader_test.go b/config/loader_test.go index 8b0a072..8aa3821 100644 --- a/config/loader_test.go +++ b/config/loader_test.go @@ -18,7 +18,7 @@ func TestLoadConfig(t *testing.T) { FS: config.FSConfig{ Mountpoint: "/tmp/gitlabfs/test/mnt/gitlab", MountOptions: "nodev", - Platform: "gitlab", + Forge: "gitlab", }, Gitlab: config.GitlabClientConfig{ URL: "https://example.com", @@ -70,7 +70,7 @@ func TestMakeGitConfig(t *testing.T) { "ValidConfig": { input: &config.Config{ FS: config.FSConfig{ - Platform: "gitlab", + Forge: "gitlab", }, Git: config.GitClientConfig{ CloneLocation: "/tmp", @@ -95,7 +95,7 @@ func TestMakeGitConfig(t *testing.T) { "InvalidOnClone": { input: &config.Config{ FS: config.FSConfig{ - Platform: "gitlab", + Forge: "gitlab", }, Git: config.GitClientConfig{ CloneLocation: "/tmp", @@ -132,7 +132,7 @@ func TestMakeGitlabConfig(t *testing.T) { "ValidConfig": { input: &config.Config{ FS: config.FSConfig{ - Platform: "gitlab", + Forge: "gitlab", }, Gitlab: config.GitlabClientConfig{ URL: "https://gitlab.com", @@ -157,7 +157,7 @@ func TestMakeGitlabConfig(t *testing.T) { "InvalidPullMethod": { input: &config.Config{ FS: config.FSConfig{ - Platform: "gitlab", + Forge: "gitlab", }, Gitlab: config.GitlabClientConfig{ URL: "https://gitlab.com", diff --git a/platforms/github/client.go b/forges/github/client.go similarity index 100% rename from platforms/github/client.go rename to forges/github/client.go diff --git a/platforms/github/organization.go b/forges/github/organization.go similarity index 100% rename from platforms/github/organization.go rename to forges/github/organization.go diff --git a/platforms/github/repository.go b/forges/github/repository.go similarity index 100% rename from platforms/github/repository.go rename to forges/github/repository.go diff --git a/platforms/github/user.go b/forges/github/user.go similarity index 100% rename from platforms/github/user.go rename to forges/github/user.go diff --git a/platforms/gitlab/client.go b/forges/gitlab/client.go similarity index 100% rename from platforms/gitlab/client.go rename to forges/gitlab/client.go diff --git a/platforms/gitlab/group.go b/forges/gitlab/group.go similarity index 100% rename from platforms/gitlab/group.go rename to forges/gitlab/group.go diff --git a/platforms/gitlab/project.go b/forges/gitlab/project.go similarity index 100% rename from platforms/gitlab/project.go rename to forges/gitlab/project.go diff --git a/platforms/gitlab/user.go b/forges/gitlab/user.go similarity index 100% rename from platforms/gitlab/user.go rename to forges/gitlab/user.go diff --git a/fstree/group.go b/fstree/group.go index 9f0ade0..c0f6646 100644 --- a/fstree/group.go +++ b/fstree/group.go @@ -39,7 +39,7 @@ func newGroupNodeFromSource(source GroupSource, param *FSParam) (*groupNode, err } func (n *groupNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { - groups, repositories, err := n.param.GitPlatform.FetchGroupContent(n.source.GetGroupID()) + groups, repositories, err := n.param.GitForge.FetchGroupContent(n.source.GetGroupID()) if err != nil { n.param.logger.Error(err.Error()) } @@ -70,7 +70,7 @@ func (n *groupNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { } func (n *groupNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { - groups, repositories, err := n.param.GitPlatform.FetchGroupContent(n.source.GetGroupID()) + groups, repositories, err := n.param.GitForge.FetchGroupContent(n.source.GetGroupID()) if err != nil { n.param.logger.Error(err.Error()) } else { diff --git a/fstree/root.go b/fstree/root.go index bb49cca..e044507 100644 --- a/fstree/root.go +++ b/fstree/root.go @@ -26,14 +26,14 @@ type GitClient interface { FetchLocalRepositoryPath(source RepositorySource) (string, error) } -type GitPlatform interface { +type GitForge interface { FetchRootGroupContent() (map[string]GroupSource, error) FetchGroupContent(gid uint64) (map[string]GroupSource, map[string]RepositorySource, error) } type FSParam struct { - GitClient GitClient - GitPlatform GitPlatform + GitClient GitClient + GitForge GitForge logger *slog.Logger staticInoChan chan uint64 @@ -77,7 +77,7 @@ func Start(logger *slog.Logger, mountpoint string, mountoptions []string, param } func (n *rootNode) OnAdd(ctx context.Context) { - rootGroups, err := n.param.GitPlatform.FetchRootGroupContent() + rootGroups, err := n.param.GitForge.FetchRootGroupContent() if err != nil { panic(err) } diff --git a/main.go b/main.go index 0af7f52..83fb40c 100644 --- a/main.go +++ b/main.go @@ -8,10 +8,10 @@ import ( "strings" "github.com/badjware/gitlabfs/config" + "github.com/badjware/gitlabfs/forges/github" + "github.com/badjware/gitlabfs/forges/gitlab" "github.com/badjware/gitlabfs/fstree" "github.com/badjware/gitlabfs/git" - "github.com/badjware/gitlabfs/platforms/github" - "github.com/badjware/gitlabfs/platforms/gitlab" ) func main() { @@ -65,23 +65,23 @@ func main() { } gitClient, _ := git.NewClient(logger, *gitClientParam) - var gitPlatformClient fstree.GitPlatform - if loadedConfig.FS.Platform == config.PlatformGitlab { + var gitForgeClient fstree.GitForge + if loadedConfig.FS.Forge == config.ForgeGitlab { // Create the gitlab client GitlabClientConfig, err := config.MakeGitlabConfig(loadedConfig) if err != nil { fmt.Println(err) os.Exit(1) } - gitPlatformClient, _ = gitlab.NewClient(logger, *GitlabClientConfig) - } else if loadedConfig.FS.Platform == config.PlatformGithub { + gitForgeClient, _ = gitlab.NewClient(logger, *GitlabClientConfig) + } else if loadedConfig.FS.Forge == config.ForgeGithub { // Create the github client GithubClientConfig, err := config.MakeGithubConfig(loadedConfig) if err != nil { fmt.Println(err) os.Exit(1) } - gitPlatformClient, _ = github.NewClient(logger, *GithubClientConfig) + gitForgeClient, _ = github.NewClient(logger, *GithubClientConfig) } // Start the filesystem @@ -89,7 +89,7 @@ func main() { logger, mountpoint, parsedMountoptions, - &fstree.FSParam{GitClient: gitClient, GitPlatform: gitPlatformClient}, + &fstree.FSParam{GitClient: gitClient, GitForge: gitForgeClient}, *debug, ) if err != nil { From bc8ae0a3c8308658c3d27d3d3f74a0caff78f087 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Thu, 8 Aug 2024 23:44:12 -0400 Subject: [PATCH 22/33] refactor to respect naming convention --- forges/github/client.go | 8 ++++---- forges/github/organization.go | 12 ++++++------ forges/github/user.go | 12 ++++++------ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/forges/github/client.go b/forges/github/client.go index e7f1285..d7cc429 100644 --- a/forges/github/client.go +++ b/forges/github/client.go @@ -63,8 +63,8 @@ func (c *githubClient) FetchRootGroupContent() (map[string]fstree.GroupSource, e if c.rootContent == nil { rootContent := make(map[string]fstree.GroupSource) - for _, org_name := range c.GithubClientConfig.OrgNames { - org, err := c.fetchOrganization(org_name) + for _, orgName := range c.GithubClientConfig.OrgNames { + org, err := c.fetchOrganization(orgName) if err != nil { c.logger.Warn(err.Error()) } else { @@ -72,8 +72,8 @@ func (c *githubClient) FetchRootGroupContent() (map[string]fstree.GroupSource, e } } - for _, user_name := range c.GithubClientConfig.UserNames { - user, err := c.fetchUser(user_name) + for _, userName := range c.GithubClientConfig.UserNames { + user, err := c.fetchUser(userName) if err != nil { c.logger.Warn(err.Error()) } else { diff --git a/forges/github/organization.go b/forges/github/organization.go index 49520f9..a23569a 100644 --- a/forges/github/organization.go +++ b/forges/github/organization.go @@ -31,26 +31,26 @@ func (o *Organization) InvalidateContentCache() { o.childRepositories = nil } -func (c *githubClient) fetchOrganization(org_name string) (*Organization, error) { +func (c *githubClient) fetchOrganization(orgName string) (*Organization, error) { c.organizationCacheMux.RLock() - cachedId, found := c.organizationNameToIDMap[org_name] + cachedId, found := c.organizationNameToIDMap[orgName] if found { cachedOrg := c.organizationCache[cachedId] c.organizationCacheMux.RUnlock() // if found in cache, return the cached reference - c.logger.Debug("Organization cache hit", "org_name", org_name) + c.logger.Debug("Organization cache hit", "org_name", orgName) return cachedOrg, nil } else { c.organizationCacheMux.RUnlock() - c.logger.Debug("Organization cache miss", "org_name", org_name) + c.logger.Debug("Organization cache miss", "org_name", orgName) } // If not found in cache, fetch organization infos from API - githubOrg, _, err := c.client.Organizations.Get(context.Background(), org_name) + githubOrg, _, err := c.client.Organizations.Get(context.Background(), orgName) if err != nil { - return nil, fmt.Errorf("failed to fetch organization with name %v: %v", org_name, err) + return nil, fmt.Errorf("failed to fetch organization with name %v: %v", orgName, err) } newOrg := Organization{ ID: *githubOrg.ID, diff --git a/forges/github/user.go b/forges/github/user.go index a2ed503..55c294a 100644 --- a/forges/github/user.go +++ b/forges/github/user.go @@ -31,26 +31,26 @@ func (u *User) InvalidateContentCache() { u.childRepositories = nil } -func (c *githubClient) fetchUser(user_name string) (*User, error) { +func (c *githubClient) fetchUser(userName string) (*User, error) { c.userCacheMux.RLock() - cachedId, found := c.userNameToIDMap[user_name] + cachedId, found := c.userNameToIDMap[userName] if found { cachedUser := c.userCache[cachedId] c.userCacheMux.RUnlock() // if found in cache, return the cached reference - c.logger.Debug("User cache hit", "user_name", user_name) + c.logger.Debug("User cache hit", "user_name", userName) return cachedUser, nil } else { c.userCacheMux.RUnlock() - c.logger.Debug("User cache miss", "user_name", user_name) + c.logger.Debug("User cache miss", "user_name", userName) } // If not found in cache, fetch user infos from API - githubUser, _, err := c.client.Users.Get(context.Background(), user_name) + githubUser, _, err := c.client.Users.Get(context.Background(), userName) if err != nil { - return nil, fmt.Errorf("failed to fetch user with name %v: %v", user_name, err) + return nil, fmt.Errorf("failed to fetch user with name %v: %v", userName, err) } newUser := User{ ID: *githubUser.ID, From ed1dc2517a562625c8fd9c3fcf067226b90c041b Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Fri, 9 Aug 2024 16:13:57 -0400 Subject: [PATCH 23/33] add gitea forge support --- config.example.yaml | 32 ++++++++++- config/config.test.yaml | 11 ++++ config/loader.go | 35 ++++++++++-- config/loader_test.go | 9 +++ forges/gitea/client.go | 96 ++++++++++++++++++++++++++++++++ forges/gitea/organization.go | 104 +++++++++++++++++++++++++++++++++++ forges/gitea/repository.go | 50 +++++++++++++++++ forges/gitea/user.go | 104 +++++++++++++++++++++++++++++++++++ go.mod | 9 ++- go.sum | 16 ++++++ main.go | 17 ++++-- 11 files changed, 472 insertions(+), 11 deletions(-) create mode 100644 forges/gitea/client.go create mode 100644 forges/gitea/organization.go create mode 100644 forges/gitea/repository.go create mode 100644 forges/gitea/user.go diff --git a/config.example.yaml b/config.example.yaml index 4dbfb1a..74591a9 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -7,7 +7,7 @@ fs: #mountoptions: nodev,nosuid # The git forge to use as the backend. - # Must be one of "gitlab" or "github" + # Must be one of "gitlab", "github", or "gitea" forge: gitlab gitlab: @@ -69,6 +69,36 @@ github: # will be automatically be added to the list of users exposed by the filesystem. include_current_user: true +gitea: + # The gitea url. + url: https://gitea.com + + # The gitlab api token + # Default to anonymous (only public repositories will be visible) + #token: + + # Must be set to either "http" or "ssh". + # The protocol to configure the git remote on. + # "http" may not work on private repositories unless a credential manager is configured + # If possible, prefer "ssh" over "http" + pull_method: http + + # A list of the name of the organizations to expose in the filesystem + org_names: [] + + # A list of the name of the user to expose their repositories un the filesystem + user_names: [] + + # Set how archived repositories are handled. + # If set to "show", it will add them to the filesystem and treat them like any other repository + # If set to "hide", it will add them to the filesystem, but prefix the symlink with a "." + # If set to "ignore", it will make them absent from the filesystem + # Default to "hide" + archived_repo_handling: hide + + # If set to true, the personal repositories and the repositories of the organizations the user the api token belongs to + # will be automatically be added to the list of users exposed by the filesystem. + include_current_user: true git: # Path to the local repository cache. Repositories in the filesystem will symlink to a folder in this path. diff --git a/config/config.test.yaml b/config/config.test.yaml index 637c992..fc28230 100644 --- a/config/config.test.yaml +++ b/config/config.test.yaml @@ -24,6 +24,17 @@ github: archived_repo_handling: hide include_current_user: true +gitea: + url: https://example.com + token: "12345" + pull_method: http + org_names: + - test-org + user_names: + - test-user + archived_repo_handling: hide + include_current_user: true + git: clone_location: /tmp/gitlabfs/test/cache/gitlab remote: origin diff --git a/config/loader.go b/config/loader.go index b785f7b..ca955b6 100644 --- a/config/loader.go +++ b/config/loader.go @@ -11,6 +11,7 @@ import ( const ( ForgeGitlab = "gitlab" ForgeGithub = "github" + ForgeGitea = "gitea" PullMethodHTTP = "http" PullMethodSSH = "ssh" @@ -25,6 +26,7 @@ type ( FS FSConfig `yaml:"fs,omitempty"` Gitlab GitlabClientConfig `yaml:"gitlab,omitempty"` Github GithubClientConfig `yaml:"github,omitempty"` + Gitea GiteaClientConfig `yaml:"gitea,omitempty"` Git GitClientConfig `yaml:"git,omitempty"` } FSConfig struct { @@ -53,6 +55,17 @@ type ( IncludeCurrentUser bool `yaml:"include_current_user,omitempty"` PullMethod string `yaml:"pull_method,omitempty"` } + GiteaClientConfig struct { + URL string `yaml:"url,omitempty"` + Token string `yaml:"token,omitempty"` + + OrgNames []string `yaml:"org_names,omitempty"` + UserNames []string `yaml:"user_names,omitempty"` + + ArchivedRepoHandling string `yaml:"archived_repo_handling,omitempty"` + IncludeCurrentUser bool `yaml:"include_current_user,omitempty"` + PullMethod string `yaml:"pull_method,omitempty"` + } GitClientConfig struct { CloneLocation string `yaml:"clone_location,omitempty"` Remote string `yaml:"remote,omitempty"` @@ -120,8 +133,8 @@ func LoadConfig(configPath string) (*Config, error) { } // validate forge is set - if config.FS.Forge != ForgeGithub && config.FS.Forge != ForgeGitlab { - return nil, fmt.Errorf("fs.forge must be either \"%v\", or \"%v\"", ForgeGitlab, ForgeGithub) + if config.FS.Forge != ForgeGithub && config.FS.Forge != ForgeGitlab && config.FS.Forge != ForgeGitea { + return nil, fmt.Errorf("fs.forge must be either \"%v\", \"%v\", or \"%v\"", ForgeGitlab, ForgeGithub, ForgeGitea) } return config, nil @@ -143,18 +156,32 @@ func MakeGitlabConfig(config *Config) (*GitlabClientConfig, error) { func MakeGithubConfig(config *Config) (*GithubClientConfig, error) { // parse pull_method - if config.Gitlab.PullMethod != PullMethodHTTP && config.Gitlab.PullMethod != PullMethodSSH { + if config.Github.PullMethod != PullMethodHTTP && config.Github.PullMethod != PullMethodSSH { return nil, fmt.Errorf("github.pull_method must be either \"%v\" or \"%v\"", PullMethodHTTP, PullMethodSSH) } // parse archive_handing - if config.Gitlab.ArchivedProjectHandling != ArchivedProjectShow && config.Gitlab.ArchivedProjectHandling != ArchivedProjectHide && config.Gitlab.ArchivedProjectHandling != ArchivedProjectIgnore { + if config.Github.ArchivedRepoHandling != ArchivedProjectShow && config.Github.ArchivedRepoHandling != ArchivedProjectHide && config.Github.ArchivedRepoHandling != ArchivedProjectIgnore { return nil, fmt.Errorf("github.archived_repo_handling must be either \"%v\", \"%v\" or \"%v\"", ArchivedProjectShow, ArchivedProjectHide, ArchivedProjectIgnore) } return &config.Github, nil } +func MakeGiteaConfig(config *Config) (*GiteaClientConfig, error) { + // parse pull_method + if config.Gitea.PullMethod != PullMethodHTTP && config.Gitea.PullMethod != PullMethodSSH { + return nil, fmt.Errorf("gitea.pull_method must be either \"%v\" or \"%v\"", PullMethodHTTP, PullMethodSSH) + } + + // parse archive_handing + if config.Gitea.ArchivedRepoHandling != ArchivedProjectShow && config.Gitea.ArchivedRepoHandling != ArchivedProjectHide && config.Gitea.ArchivedRepoHandling != ArchivedProjectIgnore { + return nil, fmt.Errorf("gitea.archived_repo_handling must be either \"%v\", \"%v\" or \"%v\"", ArchivedProjectShow, ArchivedProjectHide, ArchivedProjectIgnore) + } + + return &config.Gitea, nil +} + func MakeGitConfig(config *Config) (*GitClientConfig, error) { // parse on_clone if config.Git.OnClone != "init" && config.Git.OnClone != "clone" { diff --git a/config/loader_test.go b/config/loader_test.go index 8aa3821..421a0db 100644 --- a/config/loader_test.go +++ b/config/loader_test.go @@ -37,6 +37,15 @@ func TestLoadConfig(t *testing.T) { ArchivedRepoHandling: "hide", IncludeCurrentUser: true, }, + Gitea: config.GiteaClientConfig{ + URL: "https://example.com", + Token: "12345", + PullMethod: "http", + OrgNames: []string{"test-org"}, + UserNames: []string{"test-user"}, + ArchivedRepoHandling: "hide", + IncludeCurrentUser: true, + }, Git: config.GitClientConfig{ CloneLocation: "/tmp/gitlabfs/test/cache/gitlab", Remote: "origin", diff --git a/forges/gitea/client.go b/forges/gitea/client.go new file mode 100644 index 0000000..b158786 --- /dev/null +++ b/forges/gitea/client.go @@ -0,0 +1,96 @@ +package gitea + +import ( + "fmt" + "log/slog" + "sync" + + "code.gitea.io/sdk/gitea" + "github.com/badjware/gitlabfs/config" + "github.com/badjware/gitlabfs/fstree" +) + +type giteaClient struct { + config.GiteaClientConfig + client *gitea.Client + + logger *slog.Logger + + rootContent map[string]fstree.GroupSource + + // API response cache + organizationCacheMux sync.RWMutex + organizationNameToIDMap map[string]int64 + organizationCache map[int64]*Organization + userCacheMux sync.RWMutex + userNameToIDMap map[string]int64 + userCache map[int64]*User +} + +func NewClient(logger *slog.Logger, config config.GiteaClientConfig) (*giteaClient, error) { + client, err := gitea.NewClient(config.URL, gitea.SetToken(config.Token)) + if err != nil { + return nil, fmt.Errorf("failed to create the gitea client: %v", err) + } + + giteaClient := &giteaClient{ + GiteaClientConfig: config, + client: client, + + logger: logger, + + rootContent: nil, + + organizationNameToIDMap: map[string]int64{}, + organizationCache: map[int64]*Organization{}, + userNameToIDMap: map[string]int64{}, + userCache: map[int64]*User{}, + } + + // Fetch current user and add it to the list + currentUser, _, err := client.GetMyUserInfo() + if err != nil { + logger.Warn("failed to fetch the current user:", "error", err.Error()) + } else { + giteaClient.UserNames = append(giteaClient.UserNames, *¤tUser.UserName) + } + + return giteaClient, nil +} + +func (c *giteaClient) FetchRootGroupContent() (map[string]fstree.GroupSource, error) { + if c.rootContent == nil { + rootContent := make(map[string]fstree.GroupSource) + + for _, orgName := range c.GiteaClientConfig.OrgNames { + org, err := c.fetchOrganization(orgName) + if err != nil { + c.logger.Warn(err.Error()) + } else { + rootContent[org.Name] = org + } + } + + for _, userName := range c.GiteaClientConfig.UserNames { + user, err := c.fetchUser(userName) + if err != nil { + c.logger.Warn(err.Error()) + } else { + rootContent[user.Name] = user + } + } + + c.rootContent = rootContent + } + return c.rootContent, nil +} + +func (c *giteaClient) FetchGroupContent(gid uint64) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) { + if org, found := c.organizationCache[int64(gid)]; found { + return c.fetchOrganizationContent(org) + } + if user, found := c.userCache[int64(gid)]; found { + return c.fetchUserContent(user) + } + return nil, nil, fmt.Errorf("invalid gid: %v", gid) +} diff --git a/forges/gitea/organization.go b/forges/gitea/organization.go new file mode 100644 index 0000000..5b527a0 --- /dev/null +++ b/forges/gitea/organization.go @@ -0,0 +1,104 @@ +package gitea + +import ( + "fmt" + "sync" + + "code.gitea.io/sdk/gitea" + "github.com/badjware/gitlabfs/fstree" +) + +type Organization struct { + ID int64 + Name string + + mux sync.Mutex + + // hold org content + childRepositories map[string]fstree.RepositorySource +} + +func (o *Organization) GetGroupID() uint64 { + return uint64(o.ID) +} + +func (o *Organization) InvalidateContentCache() { + o.mux.Lock() + defer o.mux.Unlock() + + // clear child repositories from cache + o.childRepositories = nil +} + +func (c *giteaClient) fetchOrganization(orgName string) (*Organization, error) { + c.organizationCacheMux.RLock() + cachedId, found := c.organizationNameToIDMap[orgName] + if found { + cachedOrg := c.organizationCache[cachedId] + c.organizationCacheMux.RUnlock() + + // if found in cache, return the cached reference + c.logger.Debug("Organization cache hit", "org_name", orgName) + return cachedOrg, nil + } else { + c.organizationCacheMux.RUnlock() + + c.logger.Debug("Organization cache miss", "org_name", orgName) + } + + // If not found in cache, fetch organization infos from API + giteaOrg, _, err := c.client.GetOrg(orgName) + if err != nil { + return nil, fmt.Errorf("failed to fetch organization with name %v: %v", orgName, err) + } + newOrg := Organization{ + ID: giteaOrg.ID, + Name: giteaOrg.UserName, + + childRepositories: nil, + } + + // save in cache + c.organizationCacheMux.Lock() + c.organizationCache[newOrg.ID] = &newOrg + c.organizationNameToIDMap[newOrg.Name] = newOrg.ID + c.organizationCacheMux.Unlock() + + return &newOrg, nil +} + +func (c *giteaClient) fetchOrganizationContent(org *Organization) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) { + org.mux.Lock() + defer org.mux.Unlock() + + // Get cached data if available + // TODO: cache cache invalidation? + if org.childRepositories == nil { + childRepositories := make(map[string]fstree.RepositorySource) + + // Fetch the organization repositories + listReposOptions := gitea.ListReposOptions{ + ListOptions: gitea.ListOptions{PageSize: 100}, + } + for { + giteaRepositories, response, err := c.client.ListOrgRepos(org.Name, gitea.ListOrgReposOptions(listReposOptions)) + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch repository in gitea: %v", err) + } + for _, giteaRepository := range giteaRepositories { + repository := c.newRepositoryFromGiteaRepository(giteaRepository) + if repository != nil { + childRepositories[repository.Path] = repository + } + } + if response.NextPage == 0 { + break + } + // Get the next page + listReposOptions.Page = response.NextPage + } + + org.childRepositories = childRepositories + } + return make(map[string]fstree.GroupSource), org.childRepositories, nil +} diff --git a/forges/gitea/repository.go b/forges/gitea/repository.go new file mode 100644 index 0000000..9ae4529 --- /dev/null +++ b/forges/gitea/repository.go @@ -0,0 +1,50 @@ +package gitea + +import ( + "path" + + "code.gitea.io/sdk/gitea" + "github.com/badjware/gitlabfs/config" +) + +type Repository struct { + ID int64 + Path string + CloneURL string + DefaultBranch string +} + +func (r *Repository) GetRepositoryID() uint64 { + return uint64(r.ID) +} + +func (r *Repository) GetCloneURL() string { + return r.CloneURL +} + +func (r *Repository) GetDefaultBranch() string { + return r.DefaultBranch +} + +func (c *giteaClient) newRepositoryFromGiteaRepository(repository *gitea.Repository) *Repository { + if c.ArchivedRepoHandling == config.ArchivedProjectIgnore && repository.Archived { + return nil + } + r := Repository{ + ID: repository.ID, + Path: repository.Name, + DefaultBranch: repository.DefaultBranch, + } + if r.DefaultBranch == "" { + r.DefaultBranch = "master" + } + if c.PullMethod == config.PullMethodSSH { + r.CloneURL = repository.SSHURL + } else { + r.CloneURL = repository.CloneURL + } + if c.ArchivedRepoHandling == config.ArchivedProjectHide && repository.Archived { + r.Path = path.Join(path.Dir(r.Path), "."+path.Base(r.Path)) + } + return &r +} diff --git a/forges/gitea/user.go b/forges/gitea/user.go new file mode 100644 index 0000000..a8079ec --- /dev/null +++ b/forges/gitea/user.go @@ -0,0 +1,104 @@ +package gitea + +import ( + "fmt" + "sync" + + "code.gitea.io/sdk/gitea" + "github.com/badjware/gitlabfs/fstree" +) + +type User struct { + ID int64 + Name string + + mux sync.Mutex + + // hold user content + childRepositories map[string]fstree.RepositorySource +} + +func (u *User) GetGroupID() uint64 { + return uint64(u.ID) +} + +func (u *User) InvalidateContentCache() { + u.mux.Lock() + defer u.mux.Unlock() + + // clear child repositories from cache + u.childRepositories = nil +} + +func (c *giteaClient) fetchUser(userName string) (*User, error) { + c.userCacheMux.RLock() + cachedId, found := c.userNameToIDMap[userName] + if found { + cachedUser := c.userCache[cachedId] + c.userCacheMux.RUnlock() + + // if found in cache, return the cached reference + c.logger.Debug("User cache hit", "user_name", userName) + return cachedUser, nil + } else { + c.userCacheMux.RUnlock() + + c.logger.Debug("User cache miss", "user_name", userName) + } + + // If not found in cache, fetch user infos from API + giteaUser, _, err := c.client.GetUserInfo(userName) + if err != nil { + return nil, fmt.Errorf("failed to fetch user with name %v: %v", userName, err) + } + newUser := User{ + ID: giteaUser.ID, + Name: giteaUser.UserName, + + childRepositories: nil, + } + + // save in cache + c.userCacheMux.Lock() + c.userCache[newUser.ID] = &newUser + c.userNameToIDMap[newUser.Name] = newUser.ID + c.userCacheMux.Unlock() + + return &newUser, nil +} + +func (c *giteaClient) fetchUserContent(user *User) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) { + user.mux.Lock() + defer user.mux.Unlock() + + // Get cached data if available + // TODO: cache cache invalidation? + if user.childRepositories == nil { + childRepositories := make(map[string]fstree.RepositorySource) + + // Fetch the user repositories + listReposOptions := gitea.ListReposOptions{ + ListOptions: gitea.ListOptions{PageSize: 100}, + } + for { + giteaRepositories, response, err := c.client.ListUserRepos(user.Name, listReposOptions) + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch repository in gitea: %v", err) + } + for _, giteaRepository := range giteaRepositories { + repository := c.newRepositoryFromGiteaRepository(giteaRepository) + if repository != nil { + childRepositories[repository.Path] = repository + } + } + if response.NextPage == 0 { + break + } + // Get the next page + listReposOptions.Page = response.NextPage + } + + user.childRepositories = childRepositories + } + return make(map[string]fstree.GroupSource), user.childRepositories, nil +} diff --git a/go.mod b/go.mod index 358f256..4cab85f 100644 --- a/go.mod +++ b/go.mod @@ -7,14 +7,17 @@ require ( github.com/vmihailenco/taskq/v3 v3.2.9-0.20211122085105-720ffc56ac4d github.com/xanzy/go-gitlab v0.47.0 gopkg.in/yaml.v2 v2.4.0 + code.gitea.io/sdk/gitea v0.19.0 ) require ( github.com/bsm/redislock v0.7.2 // indirect github.com/capnm/sysinfo v0.0.0-20130621111458-5909a53897f3 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/davidmz/go-pageant v1.0.2 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/go-fed/httpsig v1.1.0 // indirect github.com/go-redis/redis/v8 v8.11.4 // indirect github.com/go-redis/redis_rate/v9 v9.1.2 // indirect github.com/golang/protobuf v1.5.2 // indirect @@ -22,13 +25,15 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.6.8 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/klauspost/compress v1.14.4 // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/net v0.21.0 // indirect golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 // indirect - golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect + golang.org/x/sys v0.19.0 // indirect golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.26.0 // indirect diff --git a/go.sum b/go.sum index b3a093a..b643cc8 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +code.gitea.io/sdk/gitea v0.19.0 h1:8I6s1s4RHgzxiPHhOQdgim1RWIRcr0LVMbHBjBFXq4Y= +code.gitea.io/sdk/gitea v0.19.0/go.mod h1:IG9xZJoltDNeDSW0qiF2Vqx5orMWa7OhVWrjvrd5NpI= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= @@ -54,6 +56,8 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= +github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= @@ -65,6 +69,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= +github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -144,6 +150,8 @@ github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrj github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-retryablehttp v0.6.8 h1:92lWxgpa+fF3FozM4B3UZtHZMJX8T5XT+TFdCxsPyWs= github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= @@ -220,6 +228,9 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -282,9 +293,12 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -339,6 +353,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/main.go b/main.go index 83fb40c..9b2fdf2 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/badjware/gitlabfs/config" + "github.com/badjware/gitlabfs/forges/gitea" "github.com/badjware/gitlabfs/forges/github" "github.com/badjware/gitlabfs/forges/gitlab" "github.com/badjware/gitlabfs/fstree" @@ -68,20 +69,28 @@ func main() { var gitForgeClient fstree.GitForge if loadedConfig.FS.Forge == config.ForgeGitlab { // Create the gitlab client - GitlabClientConfig, err := config.MakeGitlabConfig(loadedConfig) + gitlabClientConfig, err := config.MakeGitlabConfig(loadedConfig) if err != nil { fmt.Println(err) os.Exit(1) } - gitForgeClient, _ = gitlab.NewClient(logger, *GitlabClientConfig) + gitForgeClient, _ = gitlab.NewClient(logger, *gitlabClientConfig) } else if loadedConfig.FS.Forge == config.ForgeGithub { // Create the github client - GithubClientConfig, err := config.MakeGithubConfig(loadedConfig) + githubClientConfig, err := config.MakeGithubConfig(loadedConfig) if err != nil { fmt.Println(err) os.Exit(1) } - gitForgeClient, _ = github.NewClient(logger, *GithubClientConfig) + gitForgeClient, _ = github.NewClient(logger, *githubClientConfig) + } else if loadedConfig.FS.Forge == config.ForgeGitea { + // Create the gitea client + giteaClientConfig, err := config.MakeGiteaConfig(loadedConfig) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + gitForgeClient, _ = gitea.NewClient(logger, *giteaClientConfig) } // Start the filesystem From caa030b03b4f9dace037b94bfae4c03e0fa8d1e2 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Fri, 9 Aug 2024 16:17:47 -0400 Subject: [PATCH 24/33] remove staticInoChan, as it's redundant --- fstree/refresh.go | 2 +- fstree/root.go | 20 ++------------------ 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/fstree/refresh.go b/fstree/refresh.go index ebf6fe5..25671e9 100644 --- a/fstree/refresh.go +++ b/fstree/refresh.go @@ -23,7 +23,7 @@ var _ = (fs.NodeOpener)((*refreshNode)(nil)) func newRefreshNode(source GroupSource, param *FSParam) *refreshNode { return &refreshNode{ - ino: <-param.staticInoChan, + ino: 0, source: source, } } diff --git a/fstree/root.go b/fstree/root.go index e044507..9a882f4 100644 --- a/fstree/root.go +++ b/fstree/root.go @@ -12,10 +12,6 @@ import ( "github.com/hanwen/go-fuse/v2/fuse" ) -const ( - staticInodeStart = uint64(int(^(uint(0))>>1)) + 1 -) - type staticNode interface { fs.InodeEmbedder Ino() uint64 @@ -35,8 +31,7 @@ type FSParam struct { GitClient GitClient GitForge GitForge - logger *slog.Logger - staticInoChan chan uint64 + logger *slog.Logger } type rootNode struct { @@ -54,13 +49,10 @@ func Start(logger *slog.Logger, mountpoint string, mountoptions []string, param opts.Debug = debug param.logger = logger - param.staticInoChan = make(chan uint64) root := &rootNode{ param: param, } - go staticInoGenerator(root.param.staticInoChan) - server, err := fs.Mount(mountpoint, root, opts) if err != nil { return fmt.Errorf("mount failed: %v", err) @@ -88,7 +80,7 @@ func (n *rootNode) OnAdd(ctx context.Context) { ctx, groupNode, fs.StableAttr{ - Ino: <-n.param.staticInoChan, + Ino: 0, Mode: fuse.S_IFDIR, }, ) @@ -98,14 +90,6 @@ func (n *rootNode) OnAdd(ctx context.Context) { n.param.logger.Info("Mounted and ready to use") } -func staticInoGenerator(staticInoChan chan<- uint64) { - i := staticInodeStart - for { - staticInoChan <- i - i++ - } -} - func signalHandler(logger *slog.Logger, signalChan <-chan os.Signal, server *fuse.Server) { err := server.WaitMount() if err != nil { From 70f269e25e316ba777a74b77f5a7e878edc98d2f Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Fri, 9 Aug 2024 16:34:45 -0400 Subject: [PATCH 25/33] fix inode collision --- fstree/group.go | 12 ++++++++---- fstree/repository.go | 4 ++++ fstree/root.go | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/fstree/group.go b/fstree/group.go index c0f6646..7be1bfa 100644 --- a/fstree/group.go +++ b/fstree/group.go @@ -8,6 +8,10 @@ import ( "github.com/hanwen/go-fuse/v2/fuse" ) +const ( + groupBaseInode = 1_000_000_000 +) + type groupNode struct { fs.Inode param *FSParam @@ -48,14 +52,14 @@ func (n *groupNode) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { for groupName, group := range groups { entries = append(entries, fuse.DirEntry{ Name: groupName, - Ino: group.GetGroupID(), + Ino: group.GetGroupID() + groupBaseInode, Mode: fuse.S_IFDIR, }) } for repositoryName, repository := range repositories { entries = append(entries, fuse.DirEntry{ Name: repositoryName, - Ino: repository.GetRepositoryID(), + Ino: repository.GetRepositoryID() + repositoryBaseInode, Mode: fuse.S_IFLNK, }) } @@ -78,7 +82,7 @@ func (n *groupNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) group, found := groups[name] if found { attrs := fs.StableAttr{ - Ino: group.GetGroupID(), + Ino: group.GetGroupID() + groupBaseInode, Mode: fuse.S_IFDIR, } groupNode, _ := newGroupNodeFromSource(group, n.param) @@ -89,7 +93,7 @@ func (n *groupNode) Lookup(ctx context.Context, name string, out *fuse.EntryOut) repository, found := repositories[name] if found { attrs := fs.StableAttr{ - Ino: repository.GetRepositoryID(), + Ino: repository.GetRepositoryID() + repositoryBaseInode, Mode: fuse.S_IFLNK, } repositoryNode, _ := newRepositoryNodeFromSource(repository, n.param) diff --git a/fstree/repository.go b/fstree/repository.go index 29539b0..acf70bf 100644 --- a/fstree/repository.go +++ b/fstree/repository.go @@ -7,6 +7,10 @@ import ( "github.com/hanwen/go-fuse/v2/fs" ) +const ( + repositoryBaseInode = 2_000_000_000 +) + type repositoryNode struct { fs.Inode param *FSParam diff --git a/fstree/root.go b/fstree/root.go index 9a882f4..d45e907 100644 --- a/fstree/root.go +++ b/fstree/root.go @@ -80,7 +80,7 @@ func (n *rootNode) OnAdd(ctx context.Context) { ctx, groupNode, fs.StableAttr{ - Ino: 0, + Ino: group.GetGroupID() + groupBaseInode, Mode: fuse.S_IFDIR, }, ) From 3180af5bd4706df7aae932362a6a42a82c991a0e Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Fri, 9 Aug 2024 17:12:48 -0400 Subject: [PATCH 26/33] update dependencies --- forges/gitlab/group.go | 10 +- forges/gitlab/user.go | 2 +- go.mod | 27 +-- go.sum | 423 +++++------------------------------------ 4 files changed, 72 insertions(+), 390 deletions(-) diff --git a/forges/gitlab/group.go b/forges/gitlab/group.go index 37d5507..75c6334 100644 --- a/forges/gitlab/group.go +++ b/forges/gitlab/group.go @@ -56,7 +56,7 @@ func (c *gitlabClient) fetchGroup(gid int) (*Group, error) { } // If not in cache, fetch group infos from API - gitlabGroup, _, err := c.client.Groups.GetGroup(gid) + gitlabGroup, _, err := c.client.Groups.GetGroup(gid, &gitlab.GetGroupOptions{}) if err != nil { return nil, fmt.Errorf("failed to fetch group with id %v: %v", gid, err) } @@ -127,15 +127,15 @@ func (c *gitlabClient) fetchGroupContent(group *Group) (map[string]fstree.GroupS childProjects := make(map[string]fstree.RepositorySource) // List subgroups in path - ListGroupsOpt := &gitlab.ListSubgroupsOptions{ + listGroupsOpt := &gitlab.ListSubGroupsOptions{ ListOptions: gitlab.ListOptions{ Page: 1, PerPage: 100, }, - AllAvailable: gitlab.Bool(true), + AllAvailable: gitlab.Ptr(true), } for { - gitlabGroups, response, err := c.client.Groups.ListSubgroups(group.ID, ListGroupsOpt) + gitlabGroups, response, err := c.client.Groups.ListSubGroups(group.ID, listGroupsOpt) if err != nil { return nil, nil, fmt.Errorf("failed to fetch groups in gitlab: %v", err) } @@ -147,7 +147,7 @@ func (c *gitlabClient) fetchGroupContent(group *Group) (map[string]fstree.GroupS break } // Get the next page - ListGroupsOpt.Page = response.NextPage + listGroupsOpt.Page = response.NextPage } // List projects in path diff --git a/forges/gitlab/user.go b/forges/gitlab/user.go index aed80c0..f20154b 100644 --- a/forges/gitlab/user.go +++ b/forges/gitlab/user.go @@ -45,7 +45,7 @@ func (c *gitlabClient) fetchUser(uid int) (*User, error) { } // If not found in cache, fetch group infos from API - gitlabUser, _, err := c.client.Users.GetUser(uid) + gitlabUser, _, err := c.client.Users.GetUser(uid, gitlab.GetUsersOptions{}) if err != nil { return nil, fmt.Errorf("failed to fetch user with id %v: %v", uid, err) } diff --git a/go.mod b/go.mod index 4cab85f..3743038 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,12 @@ module github.com/badjware/gitlabfs go 1.21 require ( - github.com/hanwen/go-fuse/v2 v2.1.0 - github.com/vmihailenco/taskq/v3 v3.2.9-0.20211122085105-720ffc56ac4d - github.com/xanzy/go-gitlab v0.47.0 - gopkg.in/yaml.v2 v2.4.0 code.gitea.io/sdk/gitea v0.19.0 + github.com/google/go-github/v63 v63.0.0 + github.com/hanwen/go-fuse/v2 v2.5.1 + github.com/vmihailenco/taskq/v3 v3.2.9 + github.com/xanzy/go-gitlab v0.107.0 + gopkg.in/yaml.v2 v2.4.0 ) require ( @@ -18,23 +19,23 @@ require ( github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/go-fed/httpsig v1.1.0 // indirect - github.com/go-redis/redis/v8 v8.11.4 // indirect + github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/go-redis/redis_rate/v9 v9.1.2 // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/google/go-github/v63 v63.0.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-retryablehttp v0.6.8 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect - github.com/klauspost/compress v1.14.4 // indirect + github.com/klauspost/compress v1.15.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect golang.org/x/crypto v0.22.0 // indirect golang.org/x/net v0.21.0 // indirect - golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect + golang.org/x/oauth2 v0.6.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/time v0.3.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.26.0 // indirect + google.golang.org/protobuf v1.29.1 // indirect ) diff --git a/go.sum b/go.sum index b643cc8..5d2b0d8 100644 --- a/go.sum +++ b/go.sum @@ -1,58 +1,14 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= code.gitea.io/sdk/gitea v0.19.0 h1:8I6s1s4RHgzxiPHhOQdgim1RWIRcr0LVMbHBjBFXq4Y= code.gitea.io/sdk/gitea v0.19.0/go.mod h1:IG9xZJoltDNeDSW0qiF2Vqx5orMWa7OhVWrjvrd5NpI= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/aws/aws-sdk-go v1.42.7 h1:Ee7QC4Y/eGebVGO/5IGN3fSXXSrheesZYYj2pYJG7Zk= -github.com/aws/aws-sdk-go v1.42.7/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= -github.com/bsm/ginkgo v1.16.4/go.mod h1:RabIZLzOCPghgHJKUqHZpqrQETA5AnF4aCSIYy5C1bk= -github.com/bsm/gomega v1.13.0/go.mod h1:JifAceMQ4crZIWYUKrlGcmbN3bqHogVTADMD2ATsbwk= -github.com/bsm/redislock v0.7.1/go.mod h1:TSF3xUotaocycoHjVAp535/bET+ZmvrtcyNrXc0Whm8= +github.com/aws/aws-sdk-go v1.43.45 h1:2708Bj4uV+ym62MOtBnErm/CDX61C4mFe9V2gXy1caE= +github.com/aws/aws-sdk-go v1.43.45/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/bsm/redislock v0.7.2 h1:jggqOio8JyX9FJBKIfjF3fTxAu/v7zC5mAID9LveqG4= github.com/bsm/redislock v0.7.2/go.mod h1:kS2g0Yvlymc9Dz8V3iVYAtLAaSVruYbAFdYBDrmC5WU= github.com/capnm/sysinfo v0.0.0-20130621111458-5909a53897f3 h1:IHZ1Le1ejzkmS7Si7dIzJvYDWe+BIoNmqMnfWHBZSVw= github.com/capnm/sysinfo v0.0.0-20130621111458-5909a53897f3/go.mod h1:M5XHQLu90v2JNm/bW2tdsYar+5vhV0gEcBcmDBNAN1Y= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -62,460 +18,185 @@ github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WA github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-redis/redis/v8 v8.1.0/go.mod h1:isLoQT/NFSP7V67lyvM9GmdvLdyZ7pEhsXvvyQtnQTo= -github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-redis/redis_rate/v9 v9.1.2 h1:H0l5VzoAtOE6ydd38j8MCq3ABlGLnvvbA1xDSVVCHgQ= github.com/go-redis/redis_rate/v9 v9.1.2/go.mod h1:oam2de2apSgRG8aJzwJddXbNu91Iyz1m8IKJE2vpvlQ= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v63 v63.0.0 h1:13xwK/wk9alSokujB9lJkuzdmQuVn2QCPeck76wR3nE= github.com/google/go-github/v63 v63.0.0/go.mod h1:IqbcrgUmIcEaioWrGYei/09o+ge5vhffGOcxrO0AfmA= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok= -github.com/hanwen/go-fuse/v2 v2.1.0 h1:+32ffteETaLYClUj0a3aHjZ1hOPxxaNEHiZiujuDaek= -github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hanwen/go-fuse/v2 v2.5.1 h1:OQBE8zVemSocRxA4OaFJbjJ5hlpCmIWbGr7r0M4uoQQ= +github.com/hanwen/go-fuse/v2 v2.5.1/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= -github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-retryablehttp v0.6.4/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= -github.com/hashicorp/go-retryablehttp v0.6.8 h1:92lWxgpa+fF3FozM4B3UZtHZMJX8T5XT+TFdCxsPyWs= -github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/iron-io/iron_go3 v0.0.0-20190916120531-a4a7f74b73ac h1:w5wltlINIIqRTqQ64dASrCo0fM7k9nosPbKCZnkL0W0= github.com/iron-io/iron_go3 v0.0.0-20190916120531-a4a7f74b73ac/go.mod h1:gyMTRVO+ZkEy7wQDyD++okPsBN2q127EpuShhHMWG54= -github.com/jeffh/go.bdd v0.0.0-20120717032931-88f798ee0c74/go.mod h1:qNa9FlAfO0U/qNkzYBMH1JKYRMzC+sP9IcyV4U18l98= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.14.4 h1:eijASRJcobkVtSt81Olfh7JX43osYLwy5krOJo6YEu4= -github.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/klauspost/compress v1.15.1 h1:y9FcTHGyrebwfP0ZZqFiaxTaiDnUrGkJkI+f583BL1A= +github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= +github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/vmihailenco/taskq/v3 v3.2.9-0.20211122085105-720ffc56ac4d h1:dhLHiNAeaWqhaRbetihHq5M7vBrQjrutblYqL4w4ro8= -github.com/vmihailenco/taskq/v3 v3.2.9-0.20211122085105-720ffc56ac4d/go.mod h1:IFuypxi7Y0h+PcactlQOPf92Ssxg0FWxQZ8ptxYW/Zk= -github.com/xanzy/go-gitlab v0.47.0 h1:nC35CNaGr9skHkJq1HMYZ58R7gZsy7SO37SkA2RIHbM= -github.com/xanzy/go-gitlab v0.47.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/vmihailenco/taskq/v3 v3.2.9 h1:QE1O8IJlh4xvSB9MJsnEBzNzmJc61y320xAyBeQZ/40= +github.com/vmihailenco/taskq/v3 v3.2.9/go.mod h1:ZoRbkYMZWEUKtKvYlLGKiaRQKUjdvwWAIs/WiW1Nwtg= +github.com/xanzy/go-gitlab v0.107.0 h1:P2CT9Uy9yN9lJo3FLxpMZ4xj6uWcpnigXsjvqJ6nd2Y= +github.com/xanzy/go-gitlab v0.107.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20200908183739-ae8ad444f925/go.mod h1:1phAWC201xIgDyaFpmDeZkgf70Q4Pd/CNqfRtVPtxNw= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 h1:D7nTwh4J0i+5mW4Zjzn5omvlr6YBcWywE6KOcatyNxY= -golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= -golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM= +google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 53450730c982f1568c653893a8da8e9d8faa2df3 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Tue, 13 Aug 2024 22:18:20 -0400 Subject: [PATCH 27/33] tweak host match regex --- git/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/client.go b/git/client.go index e89a4bd..8e71739 100644 --- a/git/client.go +++ b/git/client.go @@ -41,7 +41,7 @@ func NewClient(logger *slog.Logger, p config.GitClientConfig) (*gitClient, error logger: logger, - hostnameProg: regexp.MustCompile(`([a-z0-1:\-]+\.)+[a-z0-1:\-]+`), + hostnameProg: regexp.MustCompile(`([a-z0-1\-]+\.)+[a-z0-1\-]+`), queue: queueFactory.RegisterQueue(&taskq.QueueOptions{ Name: "git-queue", From d3e211e1cb58c4b34b90fe9efdd0deeef47f411a Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Tue, 13 Aug 2024 22:22:50 -0400 Subject: [PATCH 28/33] default to config.yaml --- config/loader.go | 18 ++++++++---------- main.go | 2 +- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/config/loader.go b/config/loader.go index ca955b6..442c307 100644 --- a/config/loader.go +++ b/config/loader.go @@ -119,17 +119,15 @@ func LoadConfig(configPath string) (*Config, error) { }, } - if configPath != "" { - f, err := os.Open(configPath) - if err != nil { - return nil, fmt.Errorf("failed to open config file: %v", err) - } - defer f.Close() + f, err := os.Open(configPath) + if err != nil { + return nil, fmt.Errorf("failed to open config file: %v", err) + } + defer f.Close() - d := yaml.NewDecoder(f) - if err := d.Decode(config); err != nil { - return nil, fmt.Errorf("failed to parse config file: %v", err) - } + d := yaml.NewDecoder(f) + if err := d.Decode(config); err != nil { + return nil, fmt.Errorf("failed to parse config file: %v", err) } // validate forge is set diff --git a/main.go b/main.go index 9b2fdf2..7445a68 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,7 @@ import ( ) func main() { - configPath := flag.String("config", "", "The config file") + configPath := flag.String("config", "config.yaml", "The config file") mountoptionsFlag := flag.String("o", "", "Filesystem mount options. See mount.fuse(8)") debug := flag.Bool("debug", false, "Enable debug logging") From 46217c1a9d0af911c14c439cf752850fa1da8021 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Tue, 13 Aug 2024 22:55:18 -0400 Subject: [PATCH 29/33] rename project to gitforgefs --- Dockerfile | 6 ------ Makefile | 2 +- config.example.yaml | 2 +- config/config.test.yaml | 4 ++-- config/loader.go | 2 +- config/loader_test.go | 6 +++--- .../{gitlabfs@.service => gitforgefs@.service} | 2 +- forges/gitea/client.go | 4 ++-- forges/gitea/organization.go | 2 +- forges/gitea/repository.go | 2 +- forges/gitea/user.go | 2 +- forges/github/client.go | 4 ++-- forges/github/organization.go | 2 +- forges/github/repository.go | 2 +- forges/github/user.go | 2 +- forges/gitlab/client.go | 4 ++-- forges/gitlab/group.go | 2 +- forges/gitlab/project.go | 2 +- forges/gitlab/user.go | 2 +- git/client.go | 6 +++--- git/clone.go | 2 +- git/pull.go | 2 +- go.mod | 2 +- main.go | 12 ++++++------ 24 files changed, 36 insertions(+), 42 deletions(-) delete mode 100644 Dockerfile rename contrib/systemd/{gitlabfs@.service => gitforgefs@.service} (63%) diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 32cc1cb..0000000 --- a/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM alpine - -COPY ./bin/gitlabfs /usr/bin/gitlabfs - -ENTRYPOINT ["gitlabfs"] - diff --git a/Makefile b/Makefile index ac29b4a..e41dcba 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PROGRAM := gitlabfs +PROGRAM := gitforgefs TARGET_DIR := ./bin VERSION := $(shell git describe --tags --always) diff --git a/config.example.yaml b/config.example.yaml index 74591a9..1719ec9 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -102,7 +102,7 @@ gitea: git: # Path to the local repository cache. Repositories in the filesystem will symlink to a folder in this path. - # Default to $XDG_DATA_HOME/gitlabfs, or $HOME/.local/share/gitlabfs if the environment variable $XDG_DATA_HOME is unset. + # Default to $XDG_DATA_HOME/gitforgefs, or $HOME/.local/share/gitforgefs if the environment variable $XDG_DATA_HOME is unset. #clone_location: # The name of the remote in the local clone. diff --git a/config/config.test.yaml b/config/config.test.yaml index fc28230..d74bb68 100644 --- a/config/config.test.yaml +++ b/config/config.test.yaml @@ -1,5 +1,5 @@ fs: - mountpoint: /tmp/gitlabfs/test/mnt/gitlab + mountpoint: /tmp/gitforgefs/test/mnt/gitlab mountoptions: nodev forge: gitlab @@ -36,7 +36,7 @@ gitea: include_current_user: true git: - clone_location: /tmp/gitlabfs/test/cache/gitlab + clone_location: /tmp/gitforgefs/test/cache/gitlab remote: origin on_clone: clone auto_pull: false diff --git a/config/loader.go b/config/loader.go index 442c307..4070aa2 100644 --- a/config/loader.go +++ b/config/loader.go @@ -83,7 +83,7 @@ func LoadConfig(configPath string) (*Config, error) { if dataHome == "" { dataHome = filepath.Join(os.Getenv("HOME"), ".local/share") } - defaultCloneLocation := filepath.Join(dataHome, "gitlabfs") + defaultCloneLocation := filepath.Join(dataHome, "gitforgefs") config := &Config{ FS: FSConfig{ diff --git a/config/loader_test.go b/config/loader_test.go index 421a0db..cee2618 100644 --- a/config/loader_test.go +++ b/config/loader_test.go @@ -4,7 +4,7 @@ import ( "reflect" "testing" - "github.com/badjware/gitlabfs/config" + "github.com/badjware/gitforgefs/config" ) func TestLoadConfig(t *testing.T) { @@ -16,7 +16,7 @@ func TestLoadConfig(t *testing.T) { input: "config.test.yaml", expected: &config.Config{ FS: config.FSConfig{ - Mountpoint: "/tmp/gitlabfs/test/mnt/gitlab", + Mountpoint: "/tmp/gitforgefs/test/mnt/gitlab", MountOptions: "nodev", Forge: "gitlab", }, @@ -47,7 +47,7 @@ func TestLoadConfig(t *testing.T) { IncludeCurrentUser: true, }, Git: config.GitClientConfig{ - CloneLocation: "/tmp/gitlabfs/test/cache/gitlab", + CloneLocation: "/tmp/gitforgefs/test/cache/gitlab", Remote: "origin", OnClone: "clone", AutoPull: false, diff --git a/contrib/systemd/gitlabfs@.service b/contrib/systemd/gitforgefs@.service similarity index 63% rename from contrib/systemd/gitlabfs@.service rename to contrib/systemd/gitforgefs@.service index 6ede4ab..8e9c900 100644 --- a/contrib/systemd/gitlabfs@.service +++ b/contrib/systemd/gitforgefs@.service @@ -4,7 +4,7 @@ Wants=network-online.target After=network-online.target [Service] -ExecStart=%h/go/bin/gitlabfs -config %E/gitlabfs/%i.yaml +ExecStart=%h/go/bin/gitforgefs -config %E/gitforgefs/%i.yaml [Install] WantedBy=default.target \ No newline at end of file diff --git a/forges/gitea/client.go b/forges/gitea/client.go index b158786..d87c932 100644 --- a/forges/gitea/client.go +++ b/forges/gitea/client.go @@ -6,8 +6,8 @@ import ( "sync" "code.gitea.io/sdk/gitea" - "github.com/badjware/gitlabfs/config" - "github.com/badjware/gitlabfs/fstree" + "github.com/badjware/gitforgefs/config" + "github.com/badjware/gitforgefs/fstree" ) type giteaClient struct { diff --git a/forges/gitea/organization.go b/forges/gitea/organization.go index 5b527a0..267a66a 100644 --- a/forges/gitea/organization.go +++ b/forges/gitea/organization.go @@ -5,7 +5,7 @@ import ( "sync" "code.gitea.io/sdk/gitea" - "github.com/badjware/gitlabfs/fstree" + "github.com/badjware/gitforgefs/fstree" ) type Organization struct { diff --git a/forges/gitea/repository.go b/forges/gitea/repository.go index 9ae4529..ec9088c 100644 --- a/forges/gitea/repository.go +++ b/forges/gitea/repository.go @@ -4,7 +4,7 @@ import ( "path" "code.gitea.io/sdk/gitea" - "github.com/badjware/gitlabfs/config" + "github.com/badjware/gitforgefs/config" ) type Repository struct { diff --git a/forges/gitea/user.go b/forges/gitea/user.go index a8079ec..8e41e7e 100644 --- a/forges/gitea/user.go +++ b/forges/gitea/user.go @@ -5,7 +5,7 @@ import ( "sync" "code.gitea.io/sdk/gitea" - "github.com/badjware/gitlabfs/fstree" + "github.com/badjware/gitforgefs/fstree" ) type User struct { diff --git a/forges/github/client.go b/forges/github/client.go index d7cc429..dda37a5 100644 --- a/forges/github/client.go +++ b/forges/github/client.go @@ -6,8 +6,8 @@ import ( "log/slog" "sync" - "github.com/badjware/gitlabfs/config" - "github.com/badjware/gitlabfs/fstree" + "github.com/badjware/gitforgefs/config" + "github.com/badjware/gitforgefs/fstree" "github.com/google/go-github/v63/github" ) diff --git a/forges/github/organization.go b/forges/github/organization.go index a23569a..9d1b7e0 100644 --- a/forges/github/organization.go +++ b/forges/github/organization.go @@ -5,7 +5,7 @@ import ( "fmt" "sync" - "github.com/badjware/gitlabfs/fstree" + "github.com/badjware/gitforgefs/fstree" "github.com/google/go-github/v63/github" ) diff --git a/forges/github/repository.go b/forges/github/repository.go index abbf482..f1e66ba 100644 --- a/forges/github/repository.go +++ b/forges/github/repository.go @@ -3,7 +3,7 @@ package github import ( "path" - "github.com/badjware/gitlabfs/config" + "github.com/badjware/gitforgefs/config" "github.com/google/go-github/v63/github" ) diff --git a/forges/github/user.go b/forges/github/user.go index 55c294a..1d1d644 100644 --- a/forges/github/user.go +++ b/forges/github/user.go @@ -5,7 +5,7 @@ import ( "fmt" "sync" - "github.com/badjware/gitlabfs/fstree" + "github.com/badjware/gitforgefs/fstree" "github.com/google/go-github/v63/github" ) diff --git a/forges/gitlab/client.go b/forges/gitlab/client.go index fd74ea7..70b06ab 100644 --- a/forges/gitlab/client.go +++ b/forges/gitlab/client.go @@ -6,8 +6,8 @@ import ( "slices" "sync" - "github.com/badjware/gitlabfs/config" - "github.com/badjware/gitlabfs/fstree" + "github.com/badjware/gitforgefs/config" + "github.com/badjware/gitforgefs/fstree" "github.com/xanzy/go-gitlab" ) diff --git a/forges/gitlab/group.go b/forges/gitlab/group.go index 75c6334..0cdfb13 100644 --- a/forges/gitlab/group.go +++ b/forges/gitlab/group.go @@ -4,7 +4,7 @@ import ( "fmt" "sync" - "github.com/badjware/gitlabfs/fstree" + "github.com/badjware/gitforgefs/fstree" "github.com/xanzy/go-gitlab" ) diff --git a/forges/gitlab/project.go b/forges/gitlab/project.go index d0abc65..c960d97 100644 --- a/forges/gitlab/project.go +++ b/forges/gitlab/project.go @@ -3,7 +3,7 @@ package gitlab import ( "path" - "github.com/badjware/gitlabfs/config" + "github.com/badjware/gitforgefs/config" "github.com/xanzy/go-gitlab" ) diff --git a/forges/gitlab/user.go b/forges/gitlab/user.go index f20154b..cde05c9 100644 --- a/forges/gitlab/user.go +++ b/forges/gitlab/user.go @@ -4,7 +4,7 @@ import ( "fmt" "sync" - "github.com/badjware/gitlabfs/fstree" + "github.com/badjware/gitforgefs/fstree" "github.com/xanzy/go-gitlab" ) diff --git a/git/client.go b/git/client.go index 8e71739..cf25c08 100644 --- a/git/client.go +++ b/git/client.go @@ -10,9 +10,9 @@ import ( "strconv" "time" - "github.com/badjware/gitlabfs/config" - "github.com/badjware/gitlabfs/fstree" - "github.com/badjware/gitlabfs/utils" + "github.com/badjware/gitforgefs/config" + "github.com/badjware/gitforgefs/fstree" + "github.com/badjware/gitforgefs/utils" "github.com/vmihailenco/taskq/v3" "github.com/vmihailenco/taskq/v3/memqueue" ) diff --git a/git/clone.go b/git/clone.go index eec9c78..bc85ec5 100644 --- a/git/clone.go +++ b/git/clone.go @@ -4,7 +4,7 @@ import ( "fmt" "strconv" - "github.com/badjware/gitlabfs/utils" + "github.com/badjware/gitforgefs/utils" ) func (c *gitClient) clone(url string, defaultBranch string, dst string) error { diff --git a/git/pull.go b/git/pull.go index ed5a448..5de6b83 100644 --- a/git/pull.go +++ b/git/pull.go @@ -4,7 +4,7 @@ import ( "fmt" "strconv" - "github.com/badjware/gitlabfs/utils" + "github.com/badjware/gitforgefs/utils" ) func (c *gitClient) pull(repoPath string, defaultBranch string) error { diff --git a/go.mod b/go.mod index 3743038..36e7fed 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/badjware/gitlabfs +module github.com/badjware/gitforgefs go 1.21 diff --git a/main.go b/main.go index 7445a68..d833972 100644 --- a/main.go +++ b/main.go @@ -7,12 +7,12 @@ import ( "os" "strings" - "github.com/badjware/gitlabfs/config" - "github.com/badjware/gitlabfs/forges/gitea" - "github.com/badjware/gitlabfs/forges/github" - "github.com/badjware/gitlabfs/forges/gitlab" - "github.com/badjware/gitlabfs/fstree" - "github.com/badjware/gitlabfs/git" + "github.com/badjware/gitforgefs/config" + "github.com/badjware/gitforgefs/forges/gitea" + "github.com/badjware/gitforgefs/forges/github" + "github.com/badjware/gitforgefs/forges/gitlab" + "github.com/badjware/gitforgefs/fstree" + "github.com/badjware/gitforgefs/git" ) func main() { From d1c82e13290ee11ba750b139eebcc32dbc488cef Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Tue, 13 Aug 2024 23:45:09 -0400 Subject: [PATCH 30/33] rewrite readme --- README.md | 145 ++++++++++++++++---------------------- contrib/systemd/README.md | 21 +++--- 2 files changed, 72 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 9e1dc91..06c0995 100644 --- a/README.md +++ b/README.md @@ -1,119 +1,94 @@ -# gitlabfs +# gitforgefs -`gitlabfs` allows you to mount and navigate Gitlab groups and user personal projects as a FUSE filesystem with every groups represented as a folder and every projects represented as a symlink pointing on a local clone of the project. +*Formerly gitlabfs* -Partial output of `tree`, truncated and with a max of 4 levels: +`gitforgefs` allows you to mount and navigate git forges (Github, Gitlab, Gitea, etc.) as a [FUSE](https://github.com/libfuse/libfuse) filesystem with every groups, organization, and users represented as a folder and every repositories represented as a symlink pointing on a local clone of the project. This is helpful to automate the organization of your local clones. + +To help illustrate, this is the output of `tree` in a filesystem exposing all the repositories of a github user. ``` -$ tree -L 4 +$ tree . -├── projects -│   └── gitlab-org -│   ├── 5-minute-production-app -│   │   ├── deploy-template -> /home/marchambault/.local/share/gitlabfs/gitlab.com/22487050 -│   │   ├── examples -│   │   ├── hipio -> /home/marchambault/.local/share/gitlabfs/gitlab.com/23344605 -│   │   ├── sandbox -│   │   └── static-template -> /home/marchambault/.local/share/gitlabfs/gitlab.com/23203100 -│   ├── allocations -> /home/marchambault/.local/share/gitlabfs/gitlab.com/684698 -│   ├── apilab -> /home/marchambault/.local/share/gitlabfs/gitlab.com/2383700 -│   ├── architecture -│   │   └── tasks -> /home/marchambault/.local/share/gitlabfs/gitlab.com/22351703 -│   ├── async-retrospectives -> /home/marchambault/.local/share/gitlabfs/gitlab.com/7937396 -│   ├── auto-deploy-app -> /home/marchambault/.local/share/gitlabfs/gitlab.com/6329546 -│   ├── auto-deploy-helm -> /home/marchambault/.local/share/gitlabfs/gitlab.com/3651684 -│   ├── auto-devops-v12-10 -> /home/marchambault/.local/share/gitlabfs/gitlab.com/18629149 -│   ├── backstage-changelog -> /home/marchambault/.local/share/gitlabfs/gitlab.com/7602162 -│   ├── blob-examples -> /home/marchambault/.local/share/gitlabfs/gitlab.com/3094319 -│   ├── build -│   │   ├── CNG -> /home/marchambault/.local/share/gitlabfs/gitlab.com/4359271 -│   │   ├── CNG-mirror -> /home/marchambault/.local/share/gitlabfs/gitlab.com/7682093 -│   │   ├── dsop-scripts -> /home/marchambault/.local/share/gitlabfs/gitlab.com/19310217 -│   │   ├── omnibus-mirror -│   │   └── tr-test-dependency-proxy -> /home/marchambault/.local/share/gitlabfs/gitlab.com/20085049 -│   ├── charts -│   │   ├── apparmor -> /home/marchambault/.local/share/gitlabfs/gitlab.com/18991900 -│   │   ├── auto-deploy-app -> /home/marchambault/.local/share/gitlabfs/gitlab.com/11915984 -│   │   ├── components -│   │   ├── consul -> /home/marchambault/.local/share/gitlabfs/gitlab.com/18663049 -│   │   ├── deploy-image-helm-base -> /home/marchambault/.local/share/gitlabfs/gitlab.com/7453181 -│   │   ├── elastic-stack -> /home/marchambault/.local/share/gitlabfs/gitlab.com/18439881 -│   │   ├── fluentd-elasticsearch -> /home/marchambault/.local/share/gitlabfs/gitlab.com/17253921 -│   │   ├── gitlab -> /home/marchambault/.local/share/gitlabfs/gitlab.com/3828396 -│   │   ├── gitlab-runner -> /home/marchambault/.local/share/gitlabfs/gitlab.com/6329679 -│   │   ├── knative -> /home/marchambault/.local/share/gitlabfs/gitlab.com/16590122 -│   │   └── plantuml -> /home/marchambault/.local/share/gitlabfs/gitlab.com/14372596 -│   [...] -└── users - └── badjware - └── test_project -> /home/marchambault/.local/share/gitlabfs/gitlab.com/23370783 +└── badjware + ├── aws-cloud-gaming -> /home/marchambault/.local/share/gitforgefs/github.com/257091317 + ├── certbot -> /home/marchambault/.local/share/gitforgefs/github.com/122014287 + ├── certbot-dns-cpanel -> /home/marchambault/.local/share/gitforgefs/github.com/131224547 + ├── certbot-dns-ispconfig -> /home/marchambault/.local/share/gitforgefs/github.com/227005814 + ├── CommonLibVR -> /home/marchambault/.local/share/gitforgefs/github.com/832968971 + ├── community -> /home/marchambault/.local/share/gitforgefs/github.com/424689724 + ├── docker-postal -> /home/marchambault/.local/share/gitforgefs/github.com/132605640 + ├── dotfiles -> /home/marchambault/.local/share/gitforgefs/github.com/192993195 + ├── ecommerce-exporter -> /home/marchambault/.local/share/gitforgefs/github.com/562583906 + ├── FightClub5eXML -> /home/marchambault/.local/share/gitforgefs/github.com/246177579 + ├── gitforgefs -> /home/marchambault/.local/share/gitforgefs/github.com/324617595 + ├── kustomize-plugins -> /home/marchambault/.local/share/gitforgefs/github.com/263480122 + ├── librechat-mistral -> /home/marchambault/.local/share/gitforgefs/github.com/753193720 + ├── PapyrusExtenderSSE -> /home/marchambault/.local/share/gitforgefs/github.com/832969611 + ├── Parsec-Cloud-Preparation-Tool -> /home/marchambault/.local/share/gitforgefs/github.com/258052650 + ├── po3-Tweaks -> /home/marchambault/.local/share/gitforgefs/github.com/832969112 + ├── prometheus-ecs-discovery -> /home/marchambault/.local/share/gitforgefs/github.com/187891900 + ├── simplefuse -> /home/marchambault/.local/share/gitforgefs/github.com/111226611 + ├── tmux-continuum -> /home/marchambault/.local/share/gitforgefs/github.com/160746043 + ├── ttyd -> /home/marchambault/.local/share/gitforgefs/github.com/132514236 + ├── usb-libvirt-hotplug -> /home/marchambault/.local/share/gitforgefs/github.com/128696299 + └── vfio-win10 -> /home/marchambault/.local/share/gitforgefs/github.com/388475049 -696 directories, 0 files +24 directories, 0 files ``` +## Supported forges + +Currently, the following forges are supported: + +| Forge | Name in configuration | API token permissions, if using an API key | +| ------------------------------- | --------------------- | ------------------------------------------------------ | +| [Gitlab](https://gitlab.com) | `gitlab` | `read_user`, `read_api` | +| [Github](https://github.com) | `github` | `repo` | +| [Gitea](https://gitea.com) | `gitea` | organization: `read`, repository: `read`, user: `read` | +| [Forgejo](https://forgejo.org/) | `gitea` | organization: `read`, repository: `read`, user: `read` | + +Merge requests to add support to other forges are welcome. ## Install Install [go](https://golang.org/) and run ``` sh -go install github.com/badjware/gitlabfs@latest +go install github.com/badjware/gitforgefs ``` -The executable will be in `$GOPATH/bin/gitlabfs` or `~/go/bin/gitlabfs` by default. For convenience, copy `gitlabfs` somewhere suitable or add `~/go/bin` in your `PATH`. +The executable will be in `$GOPATH/bin/gitforgefs` or `~/go/bin/gitforgefs` by default. For convenience, add `~/go/bin` in your `$PATH` if not done already. ## Usage Download the [example configuration file](./config.example.yaml) and edit the default configuration to suit your needs. -### Getting an API token - -To generate an api token, log into your Gitlab instance, and go in your user settings > Access Token. Create a personal access token with the following permissions at the minimum: -* `read_user` -* `read_api` - -### Getting the group ids - -The group id can be seen just under the name of the group in Gitlab. - -![group-id](media/group_id.jpg) - -### Getting the user ids - -Log into gitlab and go to https://gitlab.com/api/v4/users?username=USERNAME where `USERNAME` is the username of the user you wish to know the id of. The json response will contain the user id. - -See https://forum.gitlab.com/t/where-is-my-user-id-in-gitlab-com/7912 - -### Mounting the filesystem - -You can mount the filesystem with the following command: +Then, you can run gitforgefs as follows: ``` sh -~/go/bin/gitlabfs -config /path/to/your/config.yaml /path/to/mountpoint +gitforgefs -config config.yaml /path/to/mountpoint ``` -Once the filesystem is mounted, you can `cd` into it and navigate it like any other filesystem. The first time `ls` is run the list of groups and projects is fetched from Gitlab. This operation can take a few seconds and the command will appear frozen until it's completed. Subsequent `ls` will fetch from the cache and should be much faster. -If `on_clone` is set to `init` or `no-checkout`, the locally cloned project will appear empty. Simply running `git pull` manually in the project folder will sync it up with Gitlab. - -### Unmounting the filesystem - -To stop the filesystem, use the command `umount /path/to/mountpoint` to cleanly unmount the filesystem. - -If `gitlabfs` is not cleanly stopped, you might start seeing the error "transport endpoint is not connected" when trying to access the mountpoint, even preventing from mounting back the filesystem on the same mountpoint. To fix this, use `umount` as root user, eg: `sudo umount /path/to/mountpoint`. +Stopping gitforgefs will unmount the filesystem. In the event the mountpoint is stuck in a bad state (eg: due to receiving a SIGKILL), you may need to manually cleanup using `umount`: +``` sh +sudo umount /path/to/mountpoint +``` ### Running automatically on user login -See [./contrib/systemd](contrib/systemd) for instructions on how to configure a systemd service to automatically run gitlabfs on user login. +See [./contrib/systemd](contrib/systemd) for instructions on how to configure a systemd service to automatically run gitforgefs on user login. ## Caching -To reduce the number of calls to the Gitlab api and improve the responsiveness of the filesystem, `gitlabfs` will cache the content of the group in memory. If a group or project is renamed, created or deleted from Gitlab, these change will not appear in the filesystem. To force `gitlabfs` to refresh its cache, use `touch .refresh` in the folder to refresh to force `gitlabfs` to query Gitlab for the list of groups and projects again. +### Filesystem cache -While the filesystem lives in memory, the git repositories that are cloned are saved on disk. By default, they are saved in `$XDG_DATA_HOME/gitlabfs` or `$HOME/.local/share/gitlabfs`, if `$XDG_DATA_HOME` is unset. `gitlabfs` symlink to the local clone of that repo. The local clone is unaffected by project rename or archive/unarchive in Gitlab and a given project will always point to the correct local folder. +To reduce the number of calls to the APIs and improve the responsiveness of the filesystem, gitforgefs will cache the content of the forge in memory. If a group or project is renamed, created or deleted from the forge, these change will not appear in the filesystem immediately. To force gitforgefs to refresh its cache, use `touch .refresh` in the folder to signal gitforgefs to refresh this folder. -## Known issues / Future improvements -* Cache persists forever until a manual refresh is requested. Some way to automatically refresh would be nice. -* The filesystem is currently read-only. Implementing `mkdir` to create groups, `ln` or `touch` to create projects, etc. would be nice. -* Code need some cleanup and could maybe be optimized here and there. +### Local repository cache -## Building +While the filesystem lives in memory, the git repositories that are cloned are saved on disk. By default, they are saved in `$XDG_DATA_HOME/gitforgefs` or `$HOME/.local/share/gitforgefs`, if `$XDG_DATA_HOME` is unset. `gitforgefs` symlink to the local clone of that repo. The local clone is unaffected by project rename or archive/unarchive in Gitlab and a given project will always point to the correct local folder. + +## Future improvements +* Cache persists forever until a manual refresh is requested. Some way to automatically refresh after a timeout would be nice. + +## Building from the repo Simply use `make` to create the executable. The executable will be in `bin/`. diff --git a/contrib/systemd/README.md b/contrib/systemd/README.md index 5912950..30ace8e 100644 --- a/contrib/systemd/README.md +++ b/contrib/systemd/README.md @@ -1,12 +1,15 @@ -This unit file allows you to automatically start gitlabfs as a systemd unit. +This unit file allows you to automatically start gitforgefs as a systemd unit. -## Install -1. Install gitlabfs using `go get` -2. Run `which gitlabfs` to verify that gitlabfs is present in your PATH. if the command fail, you may need to add **$HOME/go/bin** to your PATH. -3. Copy **gitlabfs@.service** into **~/.config/systemd/user**. Create the folder if it does not exists. -4. Reload systemd: `systemctl --user daemon-reload` +## Setup +1. Install gitforgefs +2. Copy **gitforgefs@.service** into **$HOME/.config/systemd/user**. Create the folder if it does not exists. +``` sh +mkdir -p $HOME/.config/systemd/user +curl -o $HOME/.config/systemd/user/gitforgefs@.service https://raw.githubusercontent.com/badjware/gitlabfs/dev/contrib/systemd/gitforgefs%40.service +``` +3. Reload systemd: `systemctl --user daemon-reload` ## Usage -1. Create your gitlabfs config file in **~/.config/gitlabfs** eg: **~/.config/gitlabfs/gitlab.com.yaml**. Make sure the config file name ends with **.yaml** and a mountpoint is configured in the file. -2. Start your service with `systemctl --user start gitlabfs@.service`. eg: `systemctl --user start gitlabfs@gitlab.com.service`. Omit the **.yaml** in the name of the service. -3. Enable your service start on login with `systemctl --user enable gitlabfs@.service`. eg: `systemctl --user enable gitlabfs@gitlab.com.service` +1. Create your gitforgefs config file in **$HOME/.config/gitforgefs** eg: **$HOME/.config/gitforgefs/gitlab.com.yaml**. Make sure the config file name ends with **.yaml** and a mountpoint is configured in the file. +2. Start your service with `systemctl --user start gitforgefs@.service`. eg: `systemctl --user start gitforgefs@gitlab.com.service`. Omit the **.yaml** extension. +3. Enable your service to start on login with `systemctl --user enable gitforgefs@.service`. eg: `systemctl --user enable gitforgefs@gitlab.com.service` From e4b049611638e6533be4f0147f5af9dde9965ae9 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Wed, 14 Aug 2024 00:01:47 -0400 Subject: [PATCH 31/33] change user configuration of gitlab to user name This should be easier to configure than having to hunt down the user id --- config.example.yaml | 4 ++-- config/config.test.yaml | 4 ++-- config/loader.go | 6 +++--- config/loader_test.go | 10 +++++----- forges/gitlab/client.go | 20 +++++++++++++++++--- 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index 1719ec9..9d4568b 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -28,8 +28,8 @@ gitlab: group_ids: - 9970 # gitlab-org - # A list of the user ids to expose their personal projects in the filesystem. - user_ids: [] + # A list of the name of the user to expose their repositories un the filesystem + user_names: [] # Set how archived projects are handled. # If set to "show", it will add them to the filesystem and treat them like any other project diff --git a/config/config.test.yaml b/config/config.test.yaml index d74bb68..a4329c9 100644 --- a/config/config.test.yaml +++ b/config/config.test.yaml @@ -9,8 +9,8 @@ gitlab: pull_method: ssh group_ids: - 123 - user_ids: - - 456 + user_names: + - test-user archived_project_handling: hide include_current_user: true diff --git a/config/loader.go b/config/loader.go index 4070aa2..48043d8 100644 --- a/config/loader.go +++ b/config/loader.go @@ -38,8 +38,8 @@ type ( URL string `yaml:"url,omitempty"` Token string `yaml:"token,omitempty"` - GroupIDs []int `yaml:"group_ids,omitempty"` - UserIDs []int `yaml:"user_ids,omitempty"` + GroupIDs []int `yaml:"group_ids,omitempty"` + UserNames []string `yaml:"user_names,omitempty"` ArchivedProjectHandling string `yaml:"archived_project_handling,omitempty"` IncludeCurrentUser bool `yaml:"include_current_user,omitempty"` @@ -96,7 +96,7 @@ func LoadConfig(configPath string) (*Config, error) { Token: "", PullMethod: "http", GroupIDs: []int{9970}, - UserIDs: []int{}, + UserNames: []string{}, ArchivedProjectHandling: "hide", IncludeCurrentUser: true, }, diff --git a/config/loader_test.go b/config/loader_test.go index cee2618..4a33457 100644 --- a/config/loader_test.go +++ b/config/loader_test.go @@ -25,7 +25,7 @@ func TestLoadConfig(t *testing.T) { Token: "12345", PullMethod: "ssh", GroupIDs: []int{123}, - UserIDs: []int{456}, + UserNames: []int{456}, ArchivedProjectHandling: "hide", IncludeCurrentUser: true, }, @@ -148,7 +148,7 @@ func TestMakeGitlabConfig(t *testing.T) { PullMethod: "http", Token: "", GroupIDs: []int{9970}, - UserIDs: []int{}, + UserNames: []int{}, ArchivedProjectHandling: "hide", IncludeCurrentUser: true, }, @@ -158,7 +158,7 @@ func TestMakeGitlabConfig(t *testing.T) { PullMethod: "http", Token: "", GroupIDs: []int{9970}, - UserIDs: []int{}, + UserNames: []int{}, ArchivedProjectHandling: "hide", IncludeCurrentUser: true, }, @@ -173,7 +173,7 @@ func TestMakeGitlabConfig(t *testing.T) { PullMethod: "invalid", Token: "", GroupIDs: []int{9970}, - UserIDs: []int{}, + UserNames: []int{}, ArchivedProjectHandling: "hide", IncludeCurrentUser: true, }, @@ -187,7 +187,7 @@ func TestMakeGitlabConfig(t *testing.T) { PullMethod: "http", Token: "", GroupIDs: []int{9970}, - UserIDs: []int{}, + UserNames: []int{}, IncludeCurrentUser: true, ArchivedProjectHandling: "invalid", }, diff --git a/forges/gitlab/client.go b/forges/gitlab/client.go index 70b06ab..fdc3963 100644 --- a/forges/gitlab/client.go +++ b/forges/gitlab/client.go @@ -19,6 +19,8 @@ type gitlabClient struct { rootContent map[string]fstree.GroupSource + userIDs []int + // API response cache groupCacheMux sync.RWMutex groupCache map[int]*Group @@ -43,6 +45,8 @@ func NewClient(logger *slog.Logger, config config.GitlabClientConfig) (*gitlabCl rootContent: nil, + userIDs: []int{}, + groupCache: map[int]*Group{}, userCache: map[int]*User{}, } @@ -52,7 +56,17 @@ func NewClient(logger *slog.Logger, config config.GitlabClientConfig) (*gitlabCl if err != nil { logger.Warn("failed to fetch the current user:", "error", err.Error()) } else { - gitlabClient.UserIDs = append(gitlabClient.UserIDs, currentUser.ID) + gitlabClient.userIDs = append(gitlabClient.userIDs, currentUser.ID) + } + + // Fetch the configured users and add them to the list + for _, userName := range config.UserNames { + user, _, err := client.Users.ListUsers(&gitlab.ListUsersOptions{Username: &userName}) + if err != nil || len(user) != 1 { + logger.Warn("failed to fetch the user", "userName", userName, "error", err.Error()) + } else { + gitlabClient.userIDs = append(gitlabClient.userIDs, user[0].ID) + } } return gitlabClient, nil @@ -72,7 +86,7 @@ func (c *gitlabClient) FetchRootGroupContent() (map[string]fstree.GroupSource, e rootGroupCache[group.Name] = group } // fetch users - for _, uid := range c.UserIDs { + for _, uid := range c.userIDs { user, err := c.fetchUser(uid) if err != nil { return nil, err @@ -86,7 +100,7 @@ func (c *gitlabClient) FetchRootGroupContent() (map[string]fstree.GroupSource, e } func (c *gitlabClient) FetchGroupContent(gid uint64) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) { - if slices.Contains[[]int, int](c.UserIDs, int(gid)) { + if slices.Contains[[]int, int](c.userIDs, int(gid)) { // gid is a user user, err := c.fetchUser(int(gid)) if err != nil { From 331bdcd08307746412eff0fbcfac190ca5f70300 Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Wed, 14 Aug 2024 12:05:35 -0400 Subject: [PATCH 32/33] remove media folder --- media/group_id.jpg | Bin 30682 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 media/group_id.jpg diff --git a/media/group_id.jpg b/media/group_id.jpg deleted file mode 100644 index a41e9bb838c5ac1ed4e10217c4765b1923f092ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30682 zcmd421ymi)mMGk~6WkqwLvVK|xVyW%yGw9_yE_DeLy+Je+}(l&4-Wsycjw-@Gi&Z! zZ`Q1N>)+L|7w30Km@P)md3mghW$In*?SLKm>>Z zOaKBPG%|5<6joG}1^y-cUI0KP0l+N7pR)d?*niDHGBtBC0RS)(5L?K^(b*M*8-j2S z4_C)OcsdBjFtIc;1>w~ooZcC9gCKnA58e20_~{?q;x8N=gaR1Os>-6Ex?zBD5{rMr zjsFQZv2?Zr$*_TBC`|3_LFI#M{tY+%gZuu$?QGpZ_x2b3sUf17y_yOrrvUv307*a^ zkOLF}62KU61FQgBz!hKs<@O+o3!n_*i~I|Hyub7nL0ZNjEi1qTq#*`40Cs@UU;4nG zX8@7`!N0eyi#aRHUnDR@5deVNcze5k4*)R90PvFV_V!fp_V!W;01#^c&}0Aa`VRR3 zz;z3vC;U5&A`bxGg#tix|G(3W(*U3)0swH99gUog{`wyX&>7s^900D$002n~0MLH` z0KCrM+6~0}!v~5M0YDYhRopdcZk;9+2(VG-dG5fR`K5Rj13P>_&Okr5D3uuxFZF)%SP5#M3sU}4~( zVPInXDFh4xqyq^B2L%O(frNmB@!y8G?*J++m^xS|1Q;p+jtT~W3ij3ystgza4gq@U z{xrqk00I&m3K|Rs7R1E|(f=_2i3pv$~&_>Thr>dI5>zvBFP z+x{;BH4uO)HT=)QU{Twc4&_4~UekxjK5275y*s<%z{W#Nh3tEaISqk#$c9H#LjSJR zf3d^&1{CbIeyeNn>4w`AWPh$ZAujeWyQ)5uMtnDM@N*A2=E>;ttGXY>0^UwyrNFau z+2~XyA*d_<9dL4qDeuqQw_E+Q{C?frI*i;4ir#J3Ja-rV_Voh0N@m#>yFp1CgER}d z-hN!Wcv!v>+4{RKsKe(Hb3U)0-V=SO+T))*S(`ugRiHW+bh`LPHS5`c(Ro(>u=+=u zGeCUs-?acxJ@+l5?l(*CTXOE!DTzFm1iG(Z4mZ8+WPOn&=2{!<~)RDlFc3SZ*vpr!K6RLWb43x;{7*K@Osct2bAlOfkqMli2VGQIyB7W=>AOo zEXMdal2<*Og|&-$>qT~}Dk;xy1`k?=*a7nAN_k^VYE;6Y0!D23@Xom$xaGjVRQHjr zn%{H0Cxc*NQ9*Zh*$ZnZINHwW;x4jOr9(#znI%(Zoi(0iz={7dJ5CZ)jvJ$`Y3Kml#`oqQI> zP+r8)RS5KGLGO?g%#V*ld~So|dva}xpU3+TUR zKMd>B7yD?71lPzORyZwt{4c75qRmaT68JYJqq4UE??21^E8B{Ls@}*;cwZu%_e^OBhokhezZR zg<{fI@fuWg%^lz=6ISqdGO8$;$>@>@GyWQuARw4TWDOB}0KgDrl>QD(e@Ff~{|6s6 z0juYq830Bd#!Wdsz1?L0acp7%6A!qGqCP(_#P*PYf8sw2vW1`SP6%(J9BF$?1mi#0 zUbYBo&1M-V;_9wthOnY{a}_E2{c#=s0r>`8WYZZxj`Rq34aH#}H7eE>f_-@iKjfFZ6M(7+ZC`IVZsoN)%65;= zwBY}sB6~WsliF6`!vA5vOr>w-&;7;p^o;jhT=aht^ZaONuFC(OUe<_N_tWh@?sb2|;c*fPEY?wCqBZ{$Wr;s{H{uK0|Apm6L7*GIk zFi3DPkZA^4!aouq1OS6VhepLf!^A=%VP;|#Qlu~>eeXyKGJCKfYX}Af{`k=;)<2** z=Y`3Ehfn1`d~apnHdmDU12RI3Nn*^GEufkF`?B~q$T51l0KJFO%N=KOV%13jaW=>1 zKt4)(&8>5MAKx-G7s1W@N;Be=ZdEtpRFjWb7(<>kK^6H}l%YU2weEB4ki2uZ^0NBS@~rW}WuV_t z!<#|>MJv0yEC-if>4nN?kbud_frv;PhN(06^lR@!0-m02AEyFt%zmw8f)T`LlSD^* zp+O(qjETLhy$WaDbhF5*gO{N3#rGPqyJO@9;~#~0+@=DMY2cz>?cM;)ank(^Tp7Hn z$(*WdY~9$zn7Y8U7l-*;xFbJC1We}fi{6$&7GH*#S{`5aA!TBC;|Bys_rUvmgNWd< zOJYYaWL(I3gZK#a{&dVdX`%a3j{IDmZQ?!Z{6rm49n{sxVaFp>90u{7jsZ6}YFZ7EUptFNhRo6G2 zcC?G||7Cjj9Of>v^ahB<26EPXS$%Y;r%>3Ee@)+Z95rXwJpH<|yeBE7yk{T#UeBCc zcTn|+mnzR~+}vG@vM$^4Gp92$+aZ-Pz3D1#`ic33o69?XQz(K+=e+lP(6Y<5t!)r? zw1?!j^a94o@LA_b1SUhQL^ieEe5FDc6U^A8SrcjaI@%VAAv~)s0&W9oBm4M610K4= z=4t!%Y_=VHVz%alLt4I{5?#nHx3={H#+uzK7F+XB4u~21?B4+JXIH0=dmpb&BJE=t z1QWWgzBLxEEYqJ$q~lk;;d3p-dOG+>xNY5SR16@D0Tsi7_Ftbp5oFm&8$P z0`+u=5zsi9`-5AE2vbA}A*q)vC;Be8@$3xM?X!B|+QuN>Q9k?mar8ilYFe@lUx+|` z2#pR8Afk`t4^+me?@Qbq=zYjSKwBJIZ1}xfwJ}0UasCFh8g#Y(@=D&^`;~U_!0Z zH^p@$?Srfh`{**32JSFant5g-Nz+EQQ^@d&^If8**jSHR(wMH0Q)@Ar*&wV8)!er^ zjPL`Mf&`I13 z=4E;m7P^$Yd;jbekeoi$*k|y9WW;|3~XW8_FK084yBNLd@9m! zLyf2v_&E&#A*BaAHet?~4HavO!CQrrDxagrDyoM_e1s8(Y>CqN%ll*bI%i$0r z1Ck@69WP3Xj^nSMj4ahlDtyKi@rDsZO4UCoB~^q1v82&CG4e{+vxb_;f?eZLFzhMTK4`6lOSo>(vb6~ zp6V+T8u7g<*xyq<)jQk@=^=atYUR&4ZY8>H9XZ>%T+Q7Dlp->FRS8!(TO<+O!a>!; z*$~8;3Jlmp`TiS{;PlO+JLH5T5o|1JyL#pN%rwxtd>+Fk5gh!zm&Q3Ea(J=r^lX$P<18HB8W_`6PU3-E*(16=ld_ip^7a2nuqI8 z4Be4>IW(mmM1)Q4+{INZ!~JL)S@c`qsc}bYvRY~*zu1oXjQx9DYVrX z1@;8mo0#{%F1ue^b+eQQ1}~9qNYsu^DdaLgk3@`0y|RTX9$>n~dhX(+nESEKjm2rQ zl6zqinY`CL&Ctwck+3?*q`@^SYKf-t#Y(q}sx645>X{xzVe^z@ZMTe#+Di+aY&1z? zxRP#O=)kEFhro3Wy+#Aqxgvd$b6VbwzRoP5u?1UMROO6Og+#(g3;Pwk7z56%_!)pT z?dflPXEl$Y9tj1X$}S?h`nl6I={kD|ZBU5d*b-b(1_v^j4GV)l!4{nw`aK)wdqO65 zaaF21B%j%(JlY*a;ZP-QrUnEF6gj8(ewYM`MaUhn@1glZ3IrOlN&4>_cABcAr|Hpj z#i9+PCO*jptDVocto*-A-5~#(2NZ4sg8+q%U?HGk{&K*PR^uvHt$(sXDW3Kk`?a4y zZS&`3=^D>*&9yh62~t!kI3Bldh7i+4D(g{0WTJ?tD`J8dIRalDzt?MOg%(?^mXF(; z^Rug=WqA5cTnSFawLlMe3X8%wb~X()sp*RPypDE*W5L_sQA<45vP63;*k9UmDx2q7 zW3%!yezLPws$eW8qQ&YC#9c^aMz=ZR%g@ZIRQIa%&B_UHI4=B`X>kOMJUx?fo>r*p z_Gy$IN^Mxqy`Jsl1h;art7x7etxaaA6((QqscfXtuS7@u^ctcQt7zHUz@_DGMrE>mjLuJB zGmeDtfaG3O8g@mcuyG(TI0?*<3dt6`*i`*@C|km>`b6Rt>@dt0^ziI9J6gpl8$Us6 zR8>v5J|PAx&@Tq#2Lc$AZbJ(fP2sf?vy#1zP;}{*tVZ#6$yEz?^i4L~w=*a<=q-6X z=+U)ZrP9IEqRBtAOHl9s)A^wwVWA+P{?_|J0asKs3{qxDbS0BSC}Blq6cREKBd4Hg z7SJ1Tfyq?BYUmi4@K^5#|I_;~%g!!`cndw8mpKxfBt0*^nl+na_TXx4plWaf=E3bS zY2iG^J7z4vjj*HbrW$CS^m3oa_&;QRbx>r-ycC#QwQ97NbpB-PtALE~7}^P*)o6yu z4e6f5s+ZDa?CyOR)2YzpPP3gc%gZERN%H}NmP+?Wb4ZbDUFXXDQU}2*tL~9=oky8X z%B7BxW?O5u?wUHIJsmbD=BKRafr$a*gPnR8r+;P;Loy&^(Al3V_kCOD@k7Eo3Qll-Z{K<`Th()})U1Ge3!<|~JdYGXEv#1yzI0K9-9(bv$ zdSHlzRJd`kB#?Lev&pju{ZBTn=U0D z;}!?N3fpvRwcmiTSXKBSMe@36&S`^x<6Q}%C)`?EsS4v&;%Uqml&B%m22v@gg3E!E z#QKq-7m~D)zm!&nDORE`Ug=Ohwmk=kLs}^MrdAt>;CCLsE+Q{6NVrS8wOAL`xXUzN4s8b)uQUOmxR;Sb>z3+Q_1 zrCImfq$RWntaV%BD%@OX8u&BVrr}KSxJrkA`=g63e(Mgc*}a}p!^B+Cy8J8 z&fX)uYb#7du~CY%j7LdInzT7N#6=MzDx98>Ipd?Mw>#Sz^))RWP68G=)k3Kr1?*+p zd19`rK5KiYQD~$D1QYJd&1p?wj&_i8x4qS-mYVp28!3^2K@QsKC@|+=(@f0Y{05+? z4PTq9*vPGJ>DKL#>6Td0XYTj4>VM^}YVx2YNJ@`?+yORI%3i$T%+(lgi@$ zQNmW8$TvW#$@@GrTbjgPV8+O)Ior@dZZMnGG3KK>pCN%o_psWyW7@oPKIT>uKTq4c z7o~-++y8$bZ@@+Q3*y;K5oM}lJF*nr(an%i>@vIcRDi-2?hV_KAB9{etuI*;V8qOa~6Bdb;XsJz1Z&AQI&M1SUT<7m6oOu(tXzWn!A1DwpR zXP>L;*rB`K8it>D*9pRYn>f+gbBqm64;O;wdh#ITK*ZQ=a7TVC zKHbf}zU&wYGTr>&RaON18|=pZo+YZdXdlaWxvpNC9_m>ySJ<3j%6 zth1wikfrPrbH@??h%)!t&vdfe{{HF3dhUk9o<)yjRS$0c58#mRg z?fgVbq~}^o;%Je=yoo48_H`XnN9(0|n+@YfBfwH&+d0OakzJ?o!W}`wn=fp`HD5a453eYfo?sGtc8)|MMH50Tv-5{(Fw;kdF-^?~6(=%W>==Y)!y@%o^Vz&UL`N zG$dUq8xF+N$PMX~>iErruQO*wXZ1EYHa2h+-@aj#M^sAWOJcyaA5}Crasq zNjf-$N8!Hr2Q-Q35uf+-a-_2PX@d6I%YiELpq7%4MP@jw&#$#E$gpx*#Hf;bcC2JXk?%-NNA{)g;yG34hH3BInBmV5JE z_8I=FVjm5u-?fQndgLqZl1;Ah)<;D1|3rQRVBLCYWitlLSIHmCcI5CZ_3SDFXS&Mv zZ*ZFGP>{4)yZ8gDE&D3m21{U^j$Zn+EeIfSZQ7Nry? z?U3^=b8AjQ_#T6*M$sN^8m7%|$53rb$eX9vrRKhdnIx6TNDvE|P;}x)VbrpEKfA1Z zk$3Dld`Y8!PqLR8hiPeNWIENaOU{BdCEssyUe#LdPsG+l3YQ~eV+*kq<3iMAUdWU< zWj&j?#$?ERpV^|BL`2attNjqvqqZ0__1whAgl~q`uB=5N!6!Kb*E87{kfZm^$1}5h z@B1MlWks2LJA6!e3X;-G)s&JJ8Kb!+4V(4C#B;9lLmHLh2saL|#;0Z3A5Jq9Wm+bp z;j$32g3*Q!sb)x#hb%ml=%p*<@8eDf~{$PD>MH8~~e$ zI7TwoRJGM&!aL|r!7u_5uV1Due%4EPq7HEZMZ@#s%(NXVf%sT6-MBY7@ zo@nWAab!1VcMkgKwnVu0zX})LyYi@k$C(J2wEm*~weVANsD97fb#oQ+=0%Ll>fj^!&cvyZpUa(}!-;E%#uXg=l`y#Bd zJ@|OA#!ryXpB)IpJZ$|eO4OC$!7b>>9)Q^sEWOicQ=ZFCD$k3W_w}3RkTWTbdrXfQ zEOP>DoX;z!otY*1?s)b7{<@L|VaGKlQmFanPOF5KxhW4Y35_i{nEFE!w=NB?=> zvfJ_P{z|9w!M?JVN!%JT3WuvB7h-(}xW zk~D7qGQ*wA=ZXkVl25 z3UoF%B+(y=>OW36T?qRTv+LKUpOgB-a4agxS9|kuo~9O0e{-&VOr?YAT$iBrXPzS>tG=2{NwYnW$H zd1*eZe-36nrIil77Md9(OAwRH@p$l^qW!Wh$7W|i3?)TAnPs~g@LAoctu>C&S0Nmc z%j@tcuY-*|S$R98s-TY!Sxjwag6|GZx8KmJm|RgoI4Edprsn;abu%Q@6gmE!vJ*wH z=hs3@g>;MbP3y2%+HFQ-DDlz6>X46OM?VT*E8jod*+2re`OdT4#TDv{R1CA64irFdr!%a+6sBaVP`s93Q z^7(W}xa2vWObu}d_&QlN1Xjj)RB{Ys=R+l0tBvSW?8`=IJ^8^b0Km-2ExUJW>5 zT#keMTG)}>k|i@CVAtuo4!+p{u3fETv04gUbEquu{E61`?Cg+PY&?7`sE@u3(q(G< z=Hrjrw%{CipUn205BgfDoYLfqCW7XIep1!+wE(ixOv&Q#i)YZ)QdwiwD$XY^7QiqxH&YTFABCI_ftTu zlbh5uPEoQU69j>Iu^i)RbwwDVtVhpNxiO9Gz^Eg~eB#~iWmULuEGtW3ALP4_65Ai^ zSugrypxG^mx*D$>OMcXvz(9|#7j(ISJDvj401fr#&q$YI))46 z2~eyC!%tcKtap9|py+&R4%seZuxBH)prXs;@ITIEq)z@slO}6b$o4?Hx20AxG$`=~ zAe*h}7)!ea^PT4mPk0i@x%3+#X{hWCVJl*G?z-PV;R482rg(#2Set*&&6aPbMPF5QRNw1$M@?8R`T zo+a=jd5-b1EbDyy4VwB6ee&(^93@)a^>-~#>b3D8{Jtj1Us4_`3KqM_d(Ho}7{1mW z`C(A2Bgc4i^tcbwYQE*0VAO~Uu5)#}VMtgV)y>^C>J5{`kEP}MY}GTqW3wayEMoLrwzLM_H>kc*_Q+$FP92t)9r8V(dla zS6;p~Kw0AE7!c zi#$O7xhiC!FG_awe$OrSmHm*?CnJ;jb4;k3#?;i|Mohkt>d3%#4?@UPWo$PNdo8Tm z^a1X};k_yo-{lT-nE}kWfVE5g8$g^SN56v6oN{1u?e_+p2ITlVbic%3*G=5L0rjtL z4_%=6qu^vW=jQ9s!{Wiy8vt5@=Xu@?cp^SD$O0`Pn>?TVuPCN|21rh3a@YyR44NEGqYcrMlh14XM6PPZ9z&|Vf#KcBO>cUzmnW&CWhPR(>|y=U|z z`;mX7!gIr4s0E;MT_Tt#4u`RX0=^x-Pm95P)g^l}`Dgj|id)2K>-tFaExrM9(Q?v# zO2wh{usshNPds_phn>-)%!+LkRD#}3sB0s1y83*wjv1fEpT7nr$Iq1A_lQbUo4{_%|`a&mPK>3HX?4jO5@Nl&RZT_Y;i;n&qf;G4~QJWuB|`XVxtC=W4l}zRMvbyvX{3) z=Ic?Z!{VH2$XT)FO>_B*&1CaG5j@06mUv#rqiG62hG4wMp4f+h={o4tBu2p^TiM`F zBKtI6Dz(|9Jw^62Ni${H4-ONfFd!oKESG7(ncZRk!D1m2yZ5Vk;!jk>oZs;e^PSEP zWP_EP_JSSp;frMoH#vI5`$G?1@yK5rS~<=I(f!1|03RigFW!bbX<=bZi%_Z<^Dsvv<+iD$z?s2_J~4eie??q zE^aNqzhm^;Ju7$Z#k&Y-`2LP+`AApuj3iF?P< zWGg1x&p4gX`4{qV2z%*Qwo6zO)Gisy#E@Lq9|H~;jxAi%fXbcHwze>lhVV2Fk-^7t ztS{f4%8qv-96lz!{~BbRTmvbJ^gG(H`I6cU)-lA5AYxeoTwCaipsF!*@yqU5d+o17sb8MY{4^AX7dJ6}(A-5@ zCW3|ETNAV^M;?c$h~9vW>e}iRRQBd>`iB`eMEAN^tv8@eQM&`ibW-Xe`^O_`(!riA zDNmw0KkqIs?+cSCqBPxD`NO6Z&Qq-HSE{}dN?~wPcKkttHo_hyDP7!kZ+-I*@Ds>RM?46>CMklKxNp8ZW4r3iX zzo(WeoB&6Bw-H0fX09{=f%v4cFr0XJn}yMUyNlXF$;f17!a7J{FQsWZ7ut@i<_7y| ztBIS18g8*$VO4~iDwcvTB^}%f4<@!ZS$HfaF5pNPmCfAY&O5x$db|4a>ub4DW-MQq z?X@kj^(@Bp@X{n^&`Rp20@rxv*XuIZTn^P^!(H&u(gRY|C4u@739l?zK|EJ06V31B z^rgb#^Hp!aU@B)Odt$p{^Fgb*XMe_Qnc}34c7Ea~S|}>~REYbgh50p=iQx)&UEH-{wf(6gga9vXzo$IXSwf1(IG*@Et~!B>dXMO zciTFbV>^ijfN_zr=~=hfd6t*pxwTm2uT))HU9EQo?TUW#+i-ZSNM$!c7t=VtiRYZ+ z?;3Tw=td=J41=BH5D3tpC-8k2?Ri=xS{$!_dnQ3rF!kN>H?Q>+Z7UxH?f7{BQnZ7X`{JiJ-L}S=EvHnUiS7(_mDZ7Bv zUV5oyGF^~H^LHczl>M+l32cFHSAt=lF?4o=HsNy();N3IFX?q?any+2Xrgr1z&mRE zC$&LC7Wx^RU2qN@f~Pa}>Ga60b+)huCe@S8OH2n_!x`grDSH+s-{q-Vh07}T?d_Sp z;ng@T(5^nbCO2Hwcgeq_FM|4J4C7_9n?VbHH8Ooy5lx;UhizoVRz~gSKgv-WMH44i z`~=)E!nu)FYn<36JFBkm6gUI7jhpx z8UJDM((5(;9qwHnl0H#twDDBy63XT=U0(qcO7je%vNaC53|PE(6CQ z<+#)ah&ioved@?i$H~H-&9(T2Th3UgwqiupCrhTAOX-?ZE|luwEDucLNmVEH=Tyc%@OZ{Z)A{y>P8LxvZ5kh>W zn$=`tB)dOoGS?v}zNainH{^hKaT8Bs63+_q-)(Gc%Jp&4@n(sXIW?Yq0)iwRXu$|J zm!I$kEt)knb_~}uZ%>0wzj~PMa(>$^!E7>mu}e9 zulA zGa0Avr|tF1PAyY|)^s;SesP8IXu>;}rqz=0aYq`<#_%lnQHR%`ole-}leC#tCUiU% zRW3eX<;JpuM^o$WYFk8E?nKv%_-Nb7Oh;AHtzG8`bKPiBM zvY35?BS(52gc-h0r39Bz+m=tDytJCxP; z4}g@@{^0%7@w_U6ogEA3JEZn(Q#87sYjWw&R1>kC*_AF3!+5Z*F@PN5W%l`MlV{`f z+J&dKfjrpMd|UwSlxXUwJd+xm{+isQC5gmgVtC}ev$1{UmVwa~JFq^;O_!@Wq=0Dj zra9Aw1h#lemzF7qu3Va56f&QGOxB{~zt-{+_%(U$Kw2Z*GS$_-yL8wVfh|dnCy?-h zha6OJsXEM6V+%gqM$qabodJ6Z(4Pw`yZ`WWgn6cDT;64>c{7$YBeqYHchx-% zNl(6&2Q<3j;Rl;Ca%FUB`k)1pT1*|JJRf)sNbChD96HrutGhw7XqDccf>-nz9r39xgJ+)8n((MB<$+)8D|&*kH!F(18# zF|6;(=pe04Pe@_1&WcqWu+WPwi3}P#BdfmXMHmgM@Hr0Qw{WNdne+6_!%i{nAx&_e zHvJItoet69mdb{_JO|0L*6low-0I;?VQD;#>4U5%Lca7|O2x=HOWx|u!7R_W7`Qcz z1rdCx3h8qvcsk_e-gtm0-e=`0FgUA$b3mhXGg3r)vBqii)8(RW z+mBO4{4zwNFFr@0xM+uN2-c1)n(_e;XMc7~J0x14f`&3TMK5S*LB;0=;8BE=AV=Nbe^>x}tg>Onu7 z_}=*V*K4WybJ^)$0^4JS>(j4QqNu3aVDp`ydBFrB(~H!3Esy=3}KXH zo~EC;6C|klu+BwHS^I5FT7Kx7E>C;4n)WO`{E5o z$n*-y@#TbzcM;7H?33})p4ox2e(yw#emk$ddgl?wM8zVx-mY8Kx0bJV>NP{`7WMG=@Qnmtdo6;epmGuTusrE6w6^|tEcaE9tS5!>j)0ZJj+8S zL>y^*U}5p9)Q9MBp3I->z^MoBTfYI-xWH(q{L}Ht%^P3|8Eh&aF!}5tBPT+%YPK@) z&gI>UHv}WNYp|fGQYf;rFqJGPj9QMf|REn{2!WDu1#%O+BbT?stYa^PIKD#&~O2Zxow zc5`0As2*LKo(G*ZmwBC9kY5;m9Pn@pPPSjd%cJ$+5JRf|EYzf2}x&)3fr2|H!Lb0;w6LY6IwfGbn8MX50{5+DH@Zb2neS&&y#>?r{VbrXF^7QCZq8UA?yMHDxZs zM~t9#r#AP>t92KH!?7>^oCPVKA#vmPLT4+kSEsp0-%04LdE_B@e8`8|j)tK&sIDMi zh4O~cAHV}h6!zerg9H=)L4tU}-t}fdGa@oO{;cttN~}I7r>+W$=J@{i4XA2(Bs!WR zJzP9ZcaNXR4Om(a58$mcA({JfFI4GeP8=DvXex`}faUuSYdBehDC$nI`L;iFRcvCl z{PNxp2{&8Iw6NEqm8SRUbqCzR@)sk)TY&y9KDQf ziEc~#1!NzVU&vy#g%u(>^e)S<+mEhf40}$lF9E?trC8ZLooTC|J|H2oqlwe0IwlQU zRE;owskVn>w8uGo4!ClNwZc z5cEZ#9W0W3*?P^@*3u20AOb8!hT1@xFmm=v9I3tcVP=Lysg_*sM0htwMtYOH?9vQc`bp0nJcwl(L zGyIYELpX{lX)I|>n2&P&+WGyEM3!+l*kSR6uxNCUS?c3XDf`$oJnR-!_Dwt^M@fNu zMV1wJ^aX2?F4m=%i)Z}SXn49Natd=cCyqF(e9FtOxbpW9Xe1w5Md&iplRyE}n)iGx zM5GNFr1Waqx(i8Nq|Kq)WS@g4&BcnTpiQf8Yt0_iIJoCK8SPpeTf4Z@oWUofdJ{#0 zBqqfqBVg`1XqOJ@qZg}FjPpjt(Yg1%asZ0#a)3<{GghRCSGNEN9@tq93tHhBZ1Fo_ zddRg~FYr*0G?qvT!v^c=*$P{RVDQZm8%a7%BY&16p&hxhq3`2RsS%-O|5OyE>cUrx z|DHteQ_7c7$C6IXJX384YIv+5kt+C3@xFEjr9dTf4i}fYkuYX({xTe?J=GMt#Bg~c zXS79m39vRzZ2Tighw*vl41LnBZ zfu^-Rsb=-#MJ%<;;Fa}?IP)SE;KFS9(mDwT214$?CgvoAb5rp3s!_7kgVd&1Sp1|K z7{9axrv)2v5S^|0spqF8&6a&klW7d}1`m%RFQj!pc5hT4L$uPYF#JuuqFf$ZjaG$8 z8-=Hn3N(BEl)xe}D03JG%{G3~GxY{p@7{nzMYn6zg!q08>3Aj&Z!}!hA>l6K&tmO4_@HDCRxMVKj;SLj%2VAWTxPT!#>!u7-AADlv+g@;w;NH;nxKX@ z2G>J@36G*)`grjjl9{I^D8{s;3>^U>*fOk*rkY-IM=1QHIh{Ug7aM1n4IiwqezJX< zo*aLa+9-dxl)#)GPIZlvRcKqbMxw)Q-b9yfWv3ut{im=k0kun1{I-&UHgk)U`W9`< zrV!uV9L^1$R3lCA9lf7Jq;Kv*^w#&wa<$X@-fe`~p9;+zzFqx%u<22z9%~*8% z8FuTMq&!_oAvkwt-;uZT>Cw(}-#00Vh%CZ4ZqqyN;0UdYSk_rN%6+8dj$HM$x@U~M#*KjUWS$z|V+|%@gxd0hTOJlr|@K=<+7N(o>H&|J`vgG8|t{q0= z;~>OxX9wH^mll*w-8#?qs)0d015_URrj06`aE*w9uzl^x+NS^`FjGaY6x9Uh8m|y> z?95b>_Y?@mJuVZL6G5eFmislq`N$`9HkJ{V5r##oRV#yPJD6UhN<;oXbimpxn|xQt z8nN*WmduZ=6_`LP%J!M+w zloe{}Ci8a&c9Ks-lz99fz>5q|bbBn-H^rOju6YhiPY+M)=okI?|SIsv7SKQcxthuNuEO-JkgSM*fw?*{wbO-BuXspXaO7t(oyvbU&@+s7oBlxY=t6Y1 z+H6!-p{wTe|T5vq(5x`4mbxa73zHsE^2H7}&`0$|iPx&W&$p3eV z6PEcHA#M48#WSq zjrP*Nw62GyUFB2R<0c=;ufolcRH8NrJk>AM-fuwG{Tnb{wBbP8-ccK-cHTLW(V^(< zK9G~qN}aD(xPHYc98D)!HB8CFKrKV>kz^KWKsV?r%+>YryK=n&fy^NqOWPA<^=>fD zMMh>Av}%O3_%-Xu5;gcrEw?|~=G>)H>_Y86A4i-Be5LEtxM&|=c!Hybwt|;pxe#o1 zMs8a=NJDl zds(-|wOO)MC{5Uxra7b>pQbA4IFbir6*2c5Akj!2J0|oJ%6{~ezubJKV8e{n#>K#+ zA+?g!T37J%i{TM3`r!V2TjCOZ=UH)En!FvD)TL zy{^Y=xY4#SjXVd#_DGdBsoFrt*xoXuK+{Sh=(OU|x*mA#LlQ#7q%OL*1$IFbRvj9`yPJd(FJ=c~%`#>Z?rlOzacf?^ z#nM^xkFIxF^Pwrjb+-SIj|UI$#p_M{1uz|xqn&Ws^`7Ijq^^GZa91MEjKo#sbH+-> zmHF9zp4NMgnM~4Er+rJuzArZBx9s#K;l5=Jbgfwmj(cb~0_Vz3Urp;oFb;zr^Fvu| zCawi8U=NIz$LF=7ikjAG-%{DU?$%>p#s=`K(%Pshd~UrVc4hLg8dEn3qyXM;!>|i0 z8}adOgZoH|a|aI`TRj}MHz1O7J|S4e!Lui5uAJNN!+0Za8#EL34qrtxxpJxaGx4X` zy>v2KPQlem<4S5w4JE(g>>)i)r0)wMW-$Fgen&n5b3c=6dd1msMsTj;wQa_e%IjC3 zW51J6jeM_NY3(pRrS`V87#3A5(vu#%NU(V2PES6f^P~Vha0s|#m=MPwxkL( z+-^Jiwv|f@kR6nW+}S~Xbg==Lx0y8^4^?;H6P^*A@YoU0)G5ngh`#^gj-#?-7MQ;Mwk}7Kt-thQeOm{ z;Q5fO*nkVvG{*9J{{tgNdS8Q@OF<9KnegVq!D)Ba$lTcQT-8W&$v4c-lPTWgyrHe9 z*)GgXvQdPzs}BebXP3gUP4O_k+@sS%-RNu_)E2M2*;sh6I${!xxbnDS@j#+|8vGK080cN(}gH0W9h;V zAHdXuiMQ*|$1hGfD}@s{>dg2KEWl1u1@y_oKEAVUn&~PlFPnTaiZQ?Lek8q_>8Q&^ z_Bw4IZw&R3$HkKwnC^}+1=yL`xacpsN2z)Z%5fjVUp11s=W}TfUKf_+a?{_)k)bW8 zM;Yh%sCNf&tQJdWKaKv9lh&P?V6hL+@^%}x-Yz?u8n7yhxG#LE<|f!pSojm$lKOk< z`7LePa;*mzwX}@SSYiTI>N5;I@3%cAb&~m`@lT`3$Sz4^O*AxSOz#pbuMG|axzo#L zCI^pK-EP#v_J<+$rnfuK<2ydkKZq!U>Ja@X@5MBONLhR1XF!Gm%HFFwVK|Y z8E@4#IhlE1dKz--0i3f$6O|yDF_90VszOR9UgOsYQ{Gu`i5ZEzw^^v%AIYhHm+>(U|%aF=!#^F(j2W3RGwnV`@6kg)%4F)}noB=cPZXCShSAvrN#{|VOA6e)Kt z=FiQH_HcSU_4Gl#>vWA?3d;f+d|o$gIhXlt=OzP=r1VPL@x>fZ^d4-5uXr(UiI~*g9i;psRIvAzJQNlrVqAi;XDJ`7tfLQOD~>yFiTUr>w|HGv>U4Co zGEB9%$Zpc#avi3>tLbR!&&Ew~xXAThfqcW}Lso|Z$ZF~>(m$q)%ZbHrnL@XcJPYa8 z-!UG!ts`p&rqb#u$(p~lng=8 zv*`uD;3uSd!}^rh_$uDb;UO~g@|7GG6dC5ayp7aawVk!Tyu=p+_Qw4J^jC?v^nOO3 z5jyj1Rbfq`^U_$v*iQ-tpw&_|*=T)o6Z7kye`pe5ZDGHKvBKw=i*llWtHOPkT-QlX z8-jV8_C0$i;N41pX)$qWZl37vs1FVGp^p$u6mpA@DhDJkGiQhv?)O{IeGq!mZK4$L zT`n)w%{HQ1^Ox5&C!HonZ+P9gNBbzi`@VjSk$Ky=Sm65?g8yB)i2L}j#Z3U z*o`?1UKy!7GwHMfEgx%4_uod#-LEqq;mkL zwj^h*eNk-L0@gppw&s3+74fF7IOPXzmy1V_ikpqhO(SSFFVmQGww)#|G@-v8F zO9>zRY#QjYj^Eoq5ct2igIfCZ7mnOtSbX3V80$YN$&nTO4}Pj6E94){f9n3%IiWCC z6#dWTNHC_r04OFL=Z`FPX>9;ws4{EPpYmF15Euvm{f|v>Vy^#F@o#yWcPVHm2{xJk zp)Tdw6f3<`!P`HXzfA)DX8g(gKTI=1Q2M~%#{W*BNSP50hZT;^nt}s>0|9?GISI-N z0J4IWK@b=K00aFm`r&Wg|N1u+1jYuSK^XxsMu;-tuNqM%p?VTkH41+UgoXn`VO0Wf z{xbC+)ulJDxc;X8-wpXshz0O_?0%0?(x0W_Fv4*FU~CZD|4>H>WMi|!fs83YRMr1p zY$yN(hXLT&pimr?wXA55Kq)X(3@-@Pb)ncO0MOs2!m)o>k%H&XaB*EBDlYi@?Jt|6dWzBJ9B~&j;fw4D# zW<|qBb!Jevj>NO7tq;(K-$f~-yc~>Zj~Y)6v`UP1r2WfKRs8dV@{aHkqr4+P5DF9v z9UbE@P^`bbBUYYTnjy(xMnqlD6m;t||L>K`f3IKsy-*)~RrE|RbbjP!RbyW3!suqB zV!NMZdwU|1J;-e12cps#eO-G>_Myw2=aqmsvch%4zEgXO|E5t!u-2(Xm&D_ZFf82_xwU|)i zaXpawdgFK1B2_8;`nu+$f>O^eW~0kHq~8z@HJ9M>X}op@`+u+g`$$--fANsiUq&OUvDV zP$>hurMpv!t0+I|v<3sGQRJY4p zhL(WwCNbJ?@a3Abu)P!cG|2pdg|VvMM|8W_2XE3wtV%Bp-W}ayANONBbd+W5*$iCJ zgwB1pyE}FtWJjR0kvery$VHwR`6VquUVV;y$?gTEpfLX=@{_(Fb2QD$tzxq!j9)0- z@75sfg-c4)7Ib9oP@KEt2*?%wv#Ob{Q`0_tYHn+k!-$0Qt30w% z!TOBe3DEJTyO5G*TXA98EZiLJjm=p0k>NCFWGNg7eik>BQ^rq&pMsSN$HPF54s|Rkv5%O%T8;(J3qw&ttCue@YJ&o(3 zLo4HBuKT-2Z@z>b$^7eN1fl~mG0{*cvVToi6ol(vu1e43tgUL8$2X&;IgK!NkHMns2OLWw@b|Zx!u?vwEQ5fZR~rpuY>F=5%QmVe z&U&m*m|s#|N@{`oc0KC4Z(9;{^~kr)`Yw!%Ow67;NPx&d^%XyE1M<3;B6F~8ZEQ@S zT=HMZ9vo`#B>Gid#yhbXJRuAg?-3FebFE%{X5jLWyP1J(g(&tUYQV4WW4qP4S?#w! zGZI-{ydUqEn|gy0 zAuauME(9S=KKq#Cdu6MxE4b3c{6ZZ)!qgl;@N^|;H@7at+5Rp~9Lz!^ymvb#a1351V`y4#>qQ(fH#!#)i2(+?I_ARk}<8K=kn{vLG>?y z`qLlIT{C8P*=LsvFG~JXu9NX_Q6hrjm~qP_c2)mliqwq7R_d6@H^Ys-d)dyPBi&JZ zPYFC02zW~GOj(5f5{hTMP!9%WeIDM;KCmoP{ycocW|g^pP6Zk4@I0R7VRk=&VHzz& z2EZoHr(I*Fong)QV8xai?;_TfK4PpTJezs}pdG!;jMi@Pdn(AR z%+9=!ye${X8&`ekL>2n;!mtQ?R04aH_$h4Q;Fe3?pk zgHx_ubX)V%b207H;mid8Kt~r%+`cUB+Co0lw|iA;6Fj}CBM}Ps_>0a9cBCs>wp->4 zDq}J}`!nU+u_`Rge%e+m=J_xx)ROSSUeNr@(Ubfpz6xT&4=!8AQGSM>VrkO^xZdNo zVX=SrYj+Ud?An)xuN6Z>lofsYJ>nDui*4oWXjtstC~&#B=Rfe!VK!zqe3`7Hd(2Im zlq;MeVI4{;3=aA}@plBo?~^<_>MW1(H^}qfe=AexT2?)uQ9%$0Q~*SB9i!zR5fI=T z=(;EMFMz)-thekJAY=L&);tAifIFPS*aLPzOMz4LC~7L{xoMDpTAE8J0-(n!PZ6F$ zA6qcT=}%d+EbofViEODF=l3_d?;%MBh`k_z7%nj~@L(`9OW7IirS6a2>MnJ@QWQ9- z#d7kH^eX*!!4>{k2Sn_Ii-1+R2VGCNwQn6Z7%YC>-NJJlodJQWa5xdewB4G7G+N?) zvy&$IP=Q0fN2iAIS!igOaO4!*>T zbt{lTrVTuLZTo$q{@LErL+|6)64#G&f8HB^p1SvN_~O4=#>6jxEp{tjqC|~S*dLGl z2jqA9e*u_o_HJ&BJyg4YUxgUrnp(QACZ2sSp(A}@s6HETIqsb})$m1(ik|`)HYI!6 zYb917htZq_Su^b_s6~6}2Z9jO;K9K4E zIzu!RGbzO~d-wDE#P@IeE4$x{ZZEVpUk@ez2&Uv$se$g177DL_aG(7JkYLZwgFgP5 z8~pNhB#(esXw+Gb`{qgbK~p(ZtZ8NF!`PmU63*#oh92Y#$!6a##N z=&d8=G;l&6+0q+Or_33*EBBvlXmzsz+?EP7mMfuX6$M%BrzW-fvAzW>1`-y(0Gyvr zgHI_L@-Kl%N=M;;Z4y?_4`Ds}o>0P|bZ|IFVoeFv4h1m{7#Y{8HE{VEr{&g$=vW8Q zbxHN}2}9wNPoX7IWHx9O%4G0)p@MgW=w)6wN5kM z1LC+dVFq2BoYchBBaQ@j=gVem6~l_!<|&IGVnA>0DRa~lg_V$uiSmLk+})4wAx0zF zb)3ia4$CX%EY7-6&puv9N*QucD9jjNdfK1fZdBQNKfc!Z!gVMET{%yLdyGVt9&Q%0 zOyRZ6-}3OiIoS}5KrxNC{dahs0vnX)M(9w*sx3N?qt6}9F3X&V64gL8 zc1aHGgAr@|42WF0owUtDyh_kQ6;QVCmX<^hL$F|>zN>|>7C##*=^?c7mu;vxbBx+h zVx>acR{q9&9EY726Q~i}%a&J>d!r1tA(mEM#F8QZu$P+4VZb0-DfSZLt>|4_({qHTfVAP2DS!)MZd*k*y}SXlV<-G zmvgtJk82GB?N8$TCdg0nc~|~mciq6L09+kZvsyF<3LFQLFO++mCd+^{30qGcyMe@X zF$-8YW*Hb$_|b@|yOQthwn@vvq-z=s$B(AW70DT@rICeO#&0rSey@IHWq>6FC*9rg z%#~hZN!b%EFz!<^ef~s?d-zo;h&IcPM_ZIwp=gyAd@(SjlNLfeF2o`{*UzKUPgBGg zkGdscK3$5<;RhYoB*NtdBg1sy3r!%J0qQ2=rP!#va0(}2qW~q~qc0$Q*~lew2Je7) z)-KJ5^^0loGpb(P6b&wzLcGp>bCD3 zSeH*dqG(;`orD~R5#-s@WEF(G#1(A2_8s=o1ZW8Jf?{1LLu|Sgd^#essX*#uYGIBq ziEj#6mzT-$vEM%elAK`@Q|EaY0+gAjF+>G|z^Z1lDvk&^Z5nq;U2kM&N;v~4J9d+} zRNpo+1~*zOrEzLG~gA+pMWD_5lq+dAxy-mF8Xmc$m}vaJFnPSNTUXK5h&z{xhqG zjE0A+Xtj5Pnf{zKiTC=36p&sdlq02Y!2Y|WM+(I|4c58C__${g;wYg+cP4am@u<+_rs$zZTr#D z1OT?=M4ATnKK*?85(P~fs-y#x^&rDqw)*hnsyZ=Y=|R=v21PDC{059Y>oat+T5l4N z=0R^7c++0OBnCja%2nq6^BgBWWf$mU-5udEQUE$CUTz(7;Vv+s((jpc>`V~Z7e&3x zLDfy_VUKK+IrdJ;hPGl<*qtplIASaKsToTJJZ)Xdnr>*JHr2FIo^)_4X%3`gRsOz4 zU2WSy6Qc&xhPir=Z=B!@Ge>lZBP0wrqz}<3iI4FuM1g%gt*X<%f7yc;&BTpBC&|OH znko}mpp9A?W1=TAP2$^)?Ia(#Dwb9RuO%>qrXmADHJUKgsk1dzK&>~Mq6CfUyiX1U ze!|y3d#v9|BMiq(8!FPAuZ5`OEVM+Xom5f^uD8`02$h+3#SJ2nlKmL0!xfWK_(B9AkTh&nxi#gfE8T`#ufulEUb|k zS>AafoUllzQ`U+D=S_7$LvAMfdOn-*PZv5wXxFQ}0vF z;HyOpOU2Flv7-Znp+Lfjp%9->gyug*giRonNd5){C?$|DM~sKUXjD_a5CKcmuY~aM z``|g^!+iM2++;zUk)>udrsiqSj^&@>6n2XTI#4Z{80#rZg0ipnc+jxLS(V#bV}=@m zO{P4KEXAaFy6p{T$|C(DQ;?$x(9gE1vK-%&s#SXfC=9i< z3opuprU5|4{Va9PJIj%x4#!$t#gxoL1R+>x{31=gA=O1|tdS9&u6pEOvmC~QM4M5^k;N}ev^;jw5{Qa>dN*vxiDd@HVzJ4wg?gw5!0%(? zBc8{BSHK#?WL(zFyBu;!8W}Y$o7YlMIT(AQiYblsGx{mtIYvXW3C{o`TBW8Y*Ny{I z0`c*!IX$vH=0lV0{$xRP6&j}!jlyWO85$08Ia0*AM=DZpl5L(www_Eeo%bPiatPwF z93e~ejz*Y;1Jt3rdJaQ472fZ{GOF73Yb@HhQ- z_CWwA<;(e%RE*E`8D35zb&@Md1;I)2Ja}l)KBDqWU#i){yfWiV?Pe`i%aoOnUTb{i zHPGYnIPjz5q3+WJLlcsee0BTgSmIjYDUo+<9mPW=x~_q;Ouls~Xcj+Mo6ue4lSg41`rVnfc#;WKN#zuxHAdX#W4Yc>e|1#N_Y5k~sZz`sr+w>hS2LD^r;sz^K(S z_AtVup2+itJ`)7eZUofY-Itl9ER%&!`8Gb@8Rtf~V^|3inz+3=vp@vlDZMRH%r1uJ>wku@gykWA4O7W3-wk zdZqdR9XRIvx`Cw8SqS+Mnu7BFU4h{}xbPFLj)zD#bgo*w*-F$DQod){4AM3UC!$E( zy-8W{2z@olARERI(lp7UmzhD22jfYN1qNA2F+8&SoE-Bvbtm+WngxLxMs6|0aQhAotwsv3U9lb zGQZ&cxX%z6%q4Fw2Na@{<+9LOEo5ONCqh1fg_n!IQJep`s&8a^=Y4kv*lIKp9IlPr z*E#R`QCq9XZCf?lmncUes70oB**N>r=9I${jM=@2EsejmNKH2=Ve-nvjcy#krV^mt zh|IN`A0Lxr1DR+k5qcQnPc|Z(bL$(HoDD7AC*Pw`?erdg#H^s2{=k#hT903-$3ABg z^0HrD8ADxo9rz2tSg%5c*wV`x>>({rg?UTk)`-9Kf7B=chCwd8`uRAvgX}&j+9&ge zYu{f(y~L&JQwzxG&rjVWl z=`#EwdnK?4bnDcz6TizJcPmUe71& zOS;BBf#R)lN%x9zkGB`_QiK!_S&W0*#Jx5i&>(6ZC$DOhu=UZ^kZANMxFbhyogK zvx^_ROPZu_$d+cL`XfvCEw*KOrKPAe|0y=BIhm_ZBv#$e9zVmGI?tq^+nEY{H${{F zY6(!OmL^=s&4UEq{Cj+TQ@B6a=@GhcvL@>5z9MB42-Ge4T`I>U0p;+=t20~ofN6X4 zkXU1gluPeZi>sN__LZ4<#fM3muY|s;*;0_O(IvAW4wBJ(*{I9NcFnOUUk4J?g^40N zL922lqm*W-R*_o+KZTx5?bJ%EmD*kVWTLC^5;pj^1#!5p``(SS!g;CLNUXyqUv9JD zYWA#v$n8W7Z15y%lO#eLo_*>ACMN4fm7;lw`-54PabrUbtfjpST6~JRCKk7nFwo`i zBEN06OKoGH#>()fy3K-?IC(j&+VB8&>=k(hn+(~}v(DZwHx=Gc_YeN0U00&6_06&jlG z&CYthEhH|8Xa;DKm5uEZ@|?xlvg1pdpd7@1Nn5D6r2ChsOQX zOmGxO+Ub3V4#e^i?ohTpvRZGS+c=Tjx3}SJ5+bmfeH{`i$bXIc@jCj^no5zj-U&d=g#KK>1`0j|bF%T54 zI%@_aqQ#ume4ruHF?%g-KES+42b?FnWTK&M_m3cD&q9Z5TgwM#U6G~wdy(rKg&Z>H zt;bsx2f>KzOra$yMN=pHxyP6>j?o6sd#k2t!^!cT>!IW{F^6s@_pj#FN&Q_~(Avei zCNyhMuNC4td4V9kW%H_fw<{XALygtJKYX#;B4f;Ka zKc1jk$`@*odSdTC{^30D)%Jt`4(y3j9D^zJ!yE>8=$bsBUh;78QPqqKCxzWz3#(;l87k=cALw#NsrQynMN5QLtpKID2GT)_ zOj=aDv&~_3d9H~*SKjNi8;an?6b^wO>9~V2t=^){hC~KH=1(b|*U0%G%1t#M|E3Wp z2pqYeYI$l#^NNAh7>?b%QI!nT)Q-;JS%GJDV`G(g4zb>2vBXxG)L_ll{cI%fpS2L& zwP`Uq$}-xhzwJD+x!8=?5kEKBb|3v4aDxVkR6^n$zq;AG?R90A1`uK!6mS<35i9mc zmC2%;EHESx%_Y`Z13rP#=epIbaPP^#AnO?pnt_$tr1wql+K_1U-$g+9pI?$v;CljW zk_n@X?78$X2ssEhSHxwo1m9}|FsGw0m8w0^u9FaRP{}giUx1-fCA94sc5JLo-h`e{1WJ&yL*9f&yQV!_b z5>`in{g|*TUQ-6mW0B|0v&u_xM9wyOr9(o>-gJ_@FnuC}jMFZo9(;&vIvbZelwGc! zf<1ZFnapEio;2Y8QsDv5;f9aV<27E}Jw_ZLfoo9;9HNr#p#{mI-1m5!qj8M(GR-kO zco6oe%y9DOtN?(q=Orl#wy(SWdma0PYAD!HcF=O2(akEPJ=LEU-3bEDZw@=9uDzdc z>mY(h%@7z#{v9GOTkb^nRv6HDght6x=cf~|j4S=vX_S>lf&7eN=p~6V7Ku)QyHMYO zM0r>VBJ@qN4O|lAaiz`KD;?Lz9eA_PcAf@x-g4*XsoNPn3~{kso22`hsIpS z(6etV^U_PCy{Rx~MxD59)m#M^IcxjALOfl9#XGRFDS2U3#mnbNb;`9fwC? zE@0Q9QZ4Qg9o_=DE>m#D*LaWS1mL&tsvfG*B+4(59zyi!0DStA4rKKnkk(NPaw-yX zsg?#3*1;Co&!UtRGt0%CMdMm01b-@)iVc-FqoDa@Murblc;i8xk*-}-EVt^}HaQIK zP?i`TITupjEwjM2p$(bcPaqrptX3=!ROT+YclBacW9MD-O0MaybK4fN1IwfmUNU0Y z#nJ1eQ-Vt{R)DHeQt`e45zrhuS%hJ%6rj3MOYgvS*E+sAWEw5v?AjeNvdH_W;9aVO z+NXy}GS;}cZ(+$Dezk-v>u3y|+ReRlj)SYWQ}1bnR1WM7)5!7*3^zmM+5K6kj@ZW2 z>C;r05dq?(t&wTec$PC3rhIg2xbQ~oK=jFRfcZ37vVcgQKCAR|%m}(-lia%344&3; zQr5A8NVE#3+s6=-MuV>d+?pRlp)6Whb78Uexr@T)2NjPXB7(qr_(hVAxwFK&yfPkTeAB{ z?gnIKr4DQyjh$qsZo0A@-lpz7s26zhsxqvlnwCgL znOX^~%qVFU&mnA!#e$8iYAOf_icU!ura|ls;8_a@Ycz7ZC>gMS#_PlNpkt9%Vk9c$ xZ4nt>YxlTrB|Pw%KYH4>c}GfWCQNqsMWRLd2N%U>f<>}dTM6I)o71nQe*uETd+7iG From 0fb31951227be09ceb08ab66b38ead0552f45e8d Mon Sep 17 00:00:00 2001 From: Massaki Archambault Date: Wed, 14 Aug 2024 12:17:11 -0400 Subject: [PATCH 33/33] create changelog --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..06d8d40 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# v1.0.0 + +* Added support for Github forge +* Added support for Gitea/Forgejo forge +* **BREAKING** Renamed project from `gitlabfs` to `gitforgefs` +* **BREAKING** Added mandatory configuration *fs.forge* (no default) +* **BREAKING** Changed Gitlab user configuration to use user names instead of user ids +* Handle archived repo as hidden files by default +* Improved support for old version of git +* Fixed various race conditions +* Fixed inode collision issue \ No newline at end of file