Compare commits
2 Commits
0a50158239
...
2d0a62dc45
Author | SHA1 | Date |
---|---|---|
Massaki Archambault | 2d0a62dc45 | |
Massaki Archambault | 6d0d3fdfc0 |
|
@ -27,6 +27,13 @@ gitlab:
|
||||||
# A list of the user ids to expose their personal projects in the filesystem.
|
# A list of the user ids to expose their personal projects in the filesystem.
|
||||||
user_ids: []
|
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.
|
# 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
|
include_current_user: true
|
||||||
|
|
||||||
|
|
|
@ -36,12 +36,13 @@ func LoadConfig(configPath string) (*Config, error) {
|
||||||
MountOptions: "nodev,nosuid",
|
MountOptions: "nodev,nosuid",
|
||||||
},
|
},
|
||||||
Gitlab: gitlab.GitlabClientConfig{
|
Gitlab: gitlab.GitlabClientConfig{
|
||||||
URL: "https://gitlab.com",
|
URL: "https://gitlab.com",
|
||||||
Token: "",
|
Token: "",
|
||||||
PullMethod: "http",
|
PullMethod: "http",
|
||||||
GroupIDs: []int{9970},
|
GroupIDs: []int{9970},
|
||||||
UserIDs: []int{},
|
UserIDs: []int{},
|
||||||
IncludeCurrentUser: true,
|
ArchivedProjectHandling: "hide",
|
||||||
|
IncludeCurrentUser: true,
|
||||||
},
|
},
|
||||||
Git: git.GitClientParam{
|
Git: git.GitClientParam{
|
||||||
CloneLocation: defaultCloneLocation,
|
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)
|
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
|
return &config.Gitlab, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,12 +22,13 @@ func TestLoadConfig(t *testing.T) {
|
||||||
MountOptions: "nodev",
|
MountOptions: "nodev",
|
||||||
},
|
},
|
||||||
Gitlab: gitlab.GitlabClientConfig{
|
Gitlab: gitlab.GitlabClientConfig{
|
||||||
URL: "https://example.com",
|
URL: "https://example.com",
|
||||||
Token: "12345",
|
Token: "12345",
|
||||||
PullMethod: "ssh",
|
PullMethod: "ssh",
|
||||||
GroupIDs: []int{123},
|
GroupIDs: []int{123},
|
||||||
UserIDs: []int{456},
|
UserIDs: []int{456},
|
||||||
IncludeCurrentUser: true,
|
ArchivedProjectHandling: "hide",
|
||||||
|
IncludeCurrentUser: true,
|
||||||
},
|
},
|
||||||
Git: git.GitClientParam{
|
Git: git.GitClientParam{
|
||||||
CloneLocation: "/tmp/gitlabfs/test/clone",
|
CloneLocation: "/tmp/gitlabfs/test/clone",
|
||||||
|
@ -118,32 +119,49 @@ func TestMakeGitlabConfig(t *testing.T) {
|
||||||
"ValidConfig": {
|
"ValidConfig": {
|
||||||
input: &config.Config{
|
input: &config.Config{
|
||||||
Gitlab: gitlab.GitlabClientConfig{
|
Gitlab: gitlab.GitlabClientConfig{
|
||||||
URL: "https://gitlab.com",
|
URL: "https://gitlab.com",
|
||||||
Token: "",
|
PullMethod: "http",
|
||||||
GroupIDs: []int{9970},
|
Token: "",
|
||||||
UserIDs: []int{},
|
GroupIDs: []int{9970},
|
||||||
IncludeCurrentUser: true,
|
UserIDs: []int{},
|
||||||
PullMethod: "http",
|
ArchivedProjectHandling: "hide",
|
||||||
|
IncludeCurrentUser: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &gitlab.GitlabClientConfig{
|
expected: &gitlab.GitlabClientConfig{
|
||||||
URL: "https://gitlab.com",
|
URL: "https://gitlab.com",
|
||||||
Token: "",
|
PullMethod: "http",
|
||||||
GroupIDs: []int{9970},
|
Token: "",
|
||||||
UserIDs: []int{},
|
GroupIDs: []int{9970},
|
||||||
IncludeCurrentUser: true,
|
UserIDs: []int{},
|
||||||
PullMethod: "http",
|
ArchivedProjectHandling: "hide",
|
||||||
|
IncludeCurrentUser: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"InvalidPullMethod": {
|
"InvalidPullMethod": {
|
||||||
input: &config.Config{
|
input: &config.Config{
|
||||||
Gitlab: gitlab.GitlabClientConfig{
|
Gitlab: gitlab.GitlabClientConfig{
|
||||||
URL: "https://gitlab.com",
|
URL: "https://gitlab.com",
|
||||||
Token: "",
|
PullMethod: "invalid",
|
||||||
GroupIDs: []int{9970},
|
Token: "",
|
||||||
UserIDs: []int{},
|
GroupIDs: []int{9970},
|
||||||
IncludeCurrentUser: true,
|
UserIDs: []int{},
|
||||||
PullMethod: "invalid",
|
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,
|
expected: nil,
|
||||||
|
|
|
@ -18,7 +18,7 @@ type groupNode struct {
|
||||||
|
|
||||||
type GroupSource interface {
|
type GroupSource interface {
|
||||||
GetGroupID() uint64
|
GetGroupID() uint64
|
||||||
InvalidateCache()
|
InvalidateContentCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we are implementing the NodeReaddirer interface
|
// Ensure we are implementing the NodeReaddirer interface
|
||||||
|
|
|
@ -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) {
|
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
|
return nil, 0, 0
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"slices"
|
"slices"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/badjware/gitlabfs/fstree"
|
"github.com/badjware/gitlabfs/fstree"
|
||||||
"github.com/xanzy/go-gitlab"
|
"github.com/xanzy/go-gitlab"
|
||||||
|
@ -12,15 +13,20 @@ import (
|
||||||
const (
|
const (
|
||||||
PullMethodHTTP = "http"
|
PullMethodHTTP = "http"
|
||||||
PullMethodSSH = "ssh"
|
PullMethodSSH = "ssh"
|
||||||
|
|
||||||
|
ArchivedProjectShow = "show"
|
||||||
|
ArchivedProjectHide = "hide"
|
||||||
|
ArchivedProjectIgnore = "ignore"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GitlabClientConfig struct {
|
type GitlabClientConfig struct {
|
||||||
URL string `yaml:"url,omitempty"`
|
URL string `yaml:"url,omitempty"`
|
||||||
Token string `yaml:"token,omitempty"`
|
Token string `yaml:"token,omitempty"`
|
||||||
GroupIDs []int `yaml:"group_ids,omitempty"`
|
GroupIDs []int `yaml:"group_ids,omitempty"`
|
||||||
UserIDs []int `yaml:"user_ids,omitempty"`
|
UserIDs []int `yaml:"user_ids,omitempty"`
|
||||||
IncludeCurrentUser bool `yaml:"include_current_user,omitempty"`
|
ArchivedProjectHandling string `yaml:"archived_project_handling,omitempty"`
|
||||||
PullMethod string `yaml:"pull_method,omitempty"`
|
IncludeCurrentUser bool `yaml:"include_current_user,omitempty"`
|
||||||
|
PullMethod string `yaml:"pull_method,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type gitlabClient struct {
|
type gitlabClient struct {
|
||||||
|
@ -34,8 +40,10 @@ type gitlabClient struct {
|
||||||
currentUserCache *User
|
currentUserCache *User
|
||||||
|
|
||||||
// API response cache
|
// API response cache
|
||||||
groupCache map[int]*Group
|
groupCacheMux sync.RWMutex
|
||||||
userCache map[int]*User
|
groupCache map[int]*Group
|
||||||
|
userCacheMux sync.RWMutex
|
||||||
|
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 GitlabClientConfig) (*gitlabClient, error) {
|
||||||
|
|
|
@ -16,7 +16,7 @@ type Group struct {
|
||||||
|
|
||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
|
|
||||||
// group content
|
// hold group content
|
||||||
childGroups map[string]fstree.GroupSource
|
childGroups map[string]fstree.GroupSource
|
||||||
childProjects map[string]fstree.RepositorySource
|
childProjects map[string]fstree.RepositorySource
|
||||||
}
|
}
|
||||||
|
@ -25,15 +25,17 @@ func (g *Group) GetGroupID() uint64 {
|
||||||
return uint64(g.ID)
|
return uint64(g.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Group) InvalidateCache() {
|
func (g *Group) InvalidateContentCache() {
|
||||||
g.mux.Lock()
|
g.mux.Lock()
|
||||||
defer g.mux.Unlock()
|
defer g.mux.Unlock()
|
||||||
|
|
||||||
// clear child group from cache
|
// clear child group from cache
|
||||||
|
g.gitlabClient.groupCacheMux.Lock()
|
||||||
for _, childGroup := range g.childGroups {
|
for _, childGroup := range g.childGroups {
|
||||||
gid := int(childGroup.GetGroupID())
|
gid := int(childGroup.GetGroupID())
|
||||||
delete(g.gitlabClient.groupCache, gid)
|
delete(g.gitlabClient.groupCache, gid)
|
||||||
}
|
}
|
||||||
|
g.gitlabClient.groupCacheMux.Unlock()
|
||||||
g.childGroups = nil
|
g.childGroups = nil
|
||||||
|
|
||||||
// clear child repositories from cache
|
// clear child repositories from cache
|
||||||
|
@ -43,7 +45,9 @@ func (g *Group) InvalidateCache() {
|
||||||
func (c *gitlabClient) fetchGroup(gid int) (*Group, error) {
|
func (c *gitlabClient) fetchGroup(gid int) (*Group, error) {
|
||||||
// start by searching the cache
|
// start by searching the cache
|
||||||
// TODO: cache invalidation?
|
// TODO: cache invalidation?
|
||||||
|
c.groupCacheMux.RLock()
|
||||||
group, found := c.groupCache[gid]
|
group, found := c.groupCache[gid]
|
||||||
|
c.groupCacheMux.RUnlock()
|
||||||
if found {
|
if found {
|
||||||
c.logger.Debug("Group cache hit", "gid", gid)
|
c.logger.Debug("Group cache hit", "gid", gid)
|
||||||
return group, nil
|
return group, nil
|
||||||
|
@ -68,7 +72,9 @@ func (c *gitlabClient) fetchGroup(gid int) (*Group, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// save in cache
|
// save in cache
|
||||||
|
c.groupCacheMux.Lock()
|
||||||
c.groupCache[gid] = &newGroup
|
c.groupCache[gid] = &newGroup
|
||||||
|
c.groupCacheMux.Unlock()
|
||||||
|
|
||||||
return &newGroup, nil
|
return &newGroup, nil
|
||||||
}
|
}
|
||||||
|
@ -77,15 +83,18 @@ func (c *gitlabClient) newGroupFromGitlabGroup(gitlabGroup *gitlab.Group) (*Grou
|
||||||
gid := gitlabGroup.ID
|
gid := gitlabGroup.ID
|
||||||
|
|
||||||
// start by searching the cache
|
// start by searching the cache
|
||||||
|
c.groupCacheMux.RLock()
|
||||||
group, found := c.groupCache[gid]
|
group, found := c.groupCache[gid]
|
||||||
|
c.groupCacheMux.RUnlock()
|
||||||
if found {
|
if found {
|
||||||
|
// if found in cache, return the cached reference
|
||||||
c.logger.Debug("Group cache hit", "gid", gid)
|
c.logger.Debug("Group cache hit", "gid", gid)
|
||||||
return group, nil
|
return group, nil
|
||||||
} else {
|
} else {
|
||||||
c.logger.Debug("Group cache miss; registering group", "gid", gid)
|
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{
|
newGroup := Group{
|
||||||
ID: gitlabGroup.ID,
|
ID: gitlabGroup.ID,
|
||||||
Name: gitlabGroup.Path,
|
Name: gitlabGroup.Path,
|
||||||
|
@ -97,12 +106,17 @@ func (c *gitlabClient) newGroupFromGitlabGroup(gitlabGroup *gitlab.Group) (*Grou
|
||||||
}
|
}
|
||||||
|
|
||||||
// save in cache
|
// save in cache
|
||||||
|
c.groupCacheMux.Lock()
|
||||||
c.groupCache[gid] = &newGroup
|
c.groupCache[gid] = &newGroup
|
||||||
|
c.groupCacheMux.Unlock()
|
||||||
|
|
||||||
return &newGroup, nil
|
return &newGroup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *gitlabClient) fetchGroupContent(group *Group) (map[string]fstree.GroupSource, map[string]fstree.RepositorySource, error) {
|
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()
|
group.mux.Lock()
|
||||||
defer group.mux.Unlock()
|
defer group.mux.Unlock()
|
||||||
|
|
||||||
|
@ -149,7 +163,9 @@ func (c *gitlabClient) fetchGroupContent(group *Group) (map[string]fstree.GroupS
|
||||||
}
|
}
|
||||||
for _, gitlabProject := range gitlabProjects {
|
for _, gitlabProject := range gitlabProjects {
|
||||||
project := c.newProjectFromGitlabProject(gitlabProject)
|
project := c.newProjectFromGitlabProject(gitlabProject)
|
||||||
childProjects[project.Name] = &project
|
if project != nil {
|
||||||
|
childProjects[project.Path] = project
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if response.CurrentPage >= response.TotalPages {
|
if response.CurrentPage >= response.TotalPages {
|
||||||
break
|
break
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
package gitlab
|
package gitlab
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"path"
|
||||||
|
|
||||||
"github.com/xanzy/go-gitlab"
|
"github.com/xanzy/go-gitlab"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Project struct {
|
type Project struct {
|
||||||
ID int
|
ID int
|
||||||
Name string
|
Path string
|
||||||
CloneURL string
|
CloneURL string
|
||||||
DefaultBranch string
|
DefaultBranch string
|
||||||
}
|
}
|
||||||
|
@ -23,11 +25,14 @@ func (p *Project) GetDefaultBranch() string {
|
||||||
return p.DefaultBranch
|
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
|
// https://godoc.org/github.com/xanzy/go-gitlab#Project
|
||||||
|
if c.ArchivedProjectHandling == ArchivedProjectIgnore && project.Archived {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
p := Project{
|
p := Project{
|
||||||
ID: project.ID,
|
ID: project.ID,
|
||||||
Name: project.Path,
|
Path: project.Path,
|
||||||
DefaultBranch: project.DefaultBranch,
|
DefaultBranch: project.DefaultBranch,
|
||||||
}
|
}
|
||||||
if p.DefaultBranch == "" {
|
if p.DefaultBranch == "" {
|
||||||
|
@ -38,5 +43,8 @@ func (c *gitlabClient) newProjectFromGitlabProject(project *gitlab.Project) Proj
|
||||||
} else {
|
} else {
|
||||||
p.CloneURL = project.HTTPURLToRepo
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ type User struct {
|
||||||
|
|
||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
|
|
||||||
// user content
|
// hold user content
|
||||||
childProjects map[string]fstree.RepositorySource
|
childProjects map[string]fstree.RepositorySource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ func (u *User) GetGroupID() uint64 {
|
||||||
return uint64(u.ID)
|
return uint64(u.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) InvalidateCache() {
|
func (u *User) InvalidateContentCache() {
|
||||||
u.mux.Lock()
|
u.mux.Lock()
|
||||||
defer u.mux.Unlock()
|
defer u.mux.Unlock()
|
||||||
|
|
||||||
|
@ -33,15 +33,18 @@ func (u *User) InvalidateCache() {
|
||||||
func (c *gitlabClient) fetchUser(uid int) (*User, error) {
|
func (c *gitlabClient) fetchUser(uid int) (*User, error) {
|
||||||
// start by searching the cache
|
// start by searching the cache
|
||||||
// TODO: cache invalidation?
|
// TODO: cache invalidation?
|
||||||
|
c.userCacheMux.RLock()
|
||||||
user, found := c.userCache[uid]
|
user, found := c.userCache[uid]
|
||||||
|
c.userCacheMux.RUnlock()
|
||||||
if found {
|
if found {
|
||||||
|
// if found in cache, return the cached reference
|
||||||
c.logger.Debug("User cache hit", "uid", uid)
|
c.logger.Debug("User cache hit", "uid", uid)
|
||||||
return user, nil
|
return user, nil
|
||||||
} else {
|
} else {
|
||||||
c.logger.Debug("User cache miss", "uid", uid)
|
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)
|
gitlabUser, _, err := c.client.Users.GetUser(uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to fetch user with id %v: %v", uid, err)
|
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
|
// save in cache
|
||||||
c.userCache[uid] = &newUser
|
c.userCache[uid] = &newUser
|
||||||
|
c.userCacheMux.Unlock()
|
||||||
|
|
||||||
return &newUser, nil
|
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) {
|
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()
|
user.mux.Lock()
|
||||||
defer user.mux.Unlock()
|
defer user.mux.Unlock()
|
||||||
|
|
||||||
|
@ -98,7 +105,9 @@ func (c *gitlabClient) fetchUserContent(user *User) (map[string]fstree.GroupSour
|
||||||
}
|
}
|
||||||
for _, gitlabProject := range gitlabProjects {
|
for _, gitlabProject := range gitlabProjects {
|
||||||
project := c.newProjectFromGitlabProject(gitlabProject)
|
project := c.newProjectFromGitlabProject(gitlabProject)
|
||||||
childProjects[project.Name] = &project
|
if project != nil {
|
||||||
|
childProjects[project.Path] = project
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if response.CurrentPage >= response.TotalPages {
|
if response.CurrentPage >= response.TotalPages {
|
||||||
break
|
break
|
||||||
|
|
Loading…
Reference in New Issue