diff --git a/server/api/user.go b/server/api/user.go index ec26ae836a..305d5eb9ef 100644 --- a/server/api/user.go +++ b/server/api/user.go @@ -93,6 +93,7 @@ func GetRepos(c *gin.Context) { all, _ := strconv.ParseBool(c.Query("all")) flush, _ := strconv.ParseBool(c.Query("flush")) + var flushError error if flush || time.Unix(user.Synced, 0).Add(time.Hour*72).Before(time.Now()) { log.Debug().Msgf("sync begin: %s", user.Login) user.Synced = time.Now().Unix() @@ -110,31 +111,40 @@ func GetRepos(c *gin.Context) { Match: shared.NamespaceFilter(config.OwnersWhitelist), } - if err := sync.Sync(c, user, server.Config.FlatPermissions); err != nil { - log.Debug().Msgf("sync error: %s: %s", user.Login, err) + flushError = sync.Sync(c, user, server.Config.FlatPermissions) + if flushError != nil { + log.Debug().Msgf("sync error: %s: %s", user.Login, flushError) } else { log.Debug().Msgf("sync complete: %s", user.Login) } } - repos, err := _store.RepoList(user, true) + allRepos, err := _store.RepoList(user, true) if err != nil { c.String(500, "Error fetching repository list. %s", err) return } + var repos []*model.Repo if all { - c.JSON(http.StatusOK, repos) - return + repos = allRepos + } else { + for _, repo := range allRepos { + if repo.IsActive { + repos = append(repos, repo) + } + } } - var active []*model.Repo - for _, repo := range repos { - if repo.IsActive { - active = append(active, repo) - } + result := model.RepoList{ + Repos: repos, + } + + if flushError != nil { + result.Message = flushError.Error() } - c.JSON(http.StatusOK, active) + + c.JSON(http.StatusOK, result) } func PostToken(c *gin.Context) { diff --git a/server/model/repo.go b/server/model/repo.go index a8cd387387..80cf191d34 100644 --- a/server/model/repo.go +++ b/server/model/repo.go @@ -97,3 +97,8 @@ type RepoPatch struct { Visibility *string `json:"visibility,omitempty"` AllowPull *bool `json:"allow_pr,omitempty"` } + +type RepoList struct { + Message string `json:"message"` + Repos []*Repo `json:"repos"` +} diff --git a/server/shared/userSyncer.go b/server/shared/userSyncer.go index 5cbef9f079..95757766de 100644 --- a/server/shared/userSyncer.go +++ b/server/shared/userSyncer.go @@ -19,6 +19,8 @@ import ( "fmt" "time" + "github.com/rs/zerolog/log" + "github.com/woodpecker-ci/woodpecker/server/model" "github.com/woodpecker-ci/woodpecker/server/remote" "github.com/woodpecker-ci/woodpecker/server/store" @@ -67,6 +69,7 @@ func (s *Syncer) Sync(ctx context.Context, user *model.User, flatPermissions boo } remoteRepos := make([]*model.Repo, 0, len(repos)) + hasErrors := false for _, repo := range repos { if s.Match(repo) { repo.Perm = &model.Perm{ @@ -86,7 +89,9 @@ func (s *Syncer) Sync(ctx context.Context, user *model.User, flatPermissions boo } else { remotePerm, err := s.Remote.Perm(ctx, user, repo.Owner, repo.Name) if err != nil { - return fmt.Errorf("could not fetch permission of repo '%s': %v", repo.FullName, err) + log.Debug().Msgf("could not fetch permission of repo '%s': %v", repo.FullName, err) + hasErrors = true + continue } repo.Perm.Pull = remotePerm.Pull repo.Perm.Push = remotePerm.Push @@ -97,6 +102,7 @@ func (s *Syncer) Sync(ctx context.Context, user *model.User, flatPermissions boo } } + // still store all successfully queried repositories err = s.Store.RepoBatch(remoteRepos) if err != nil { return err @@ -113,5 +119,14 @@ func (s *Syncer) Sync(ctx context.Context, user *model.User, flatPermissions boo return nil } - return s.Perms.PermFlush(user, unix) + err = s.Perms.PermFlush(user, unix) + if err != nil { + return err + } + + if hasErrors { + return fmt.Errorf("failed to sync all repositories. See previous messages for details") + } + + return nil } diff --git a/web/src/lib/api/index.ts b/web/src/lib/api/index.ts index 164edf22ee..c1c1c05c62 100644 --- a/web/src/lib/api/index.ts +++ b/web/src/lib/api/index.ts @@ -1,14 +1,25 @@ import ApiClient, { encodeQueryString } from './client'; -import { Build, BuildFeed, BuildLog, BuildProc, Registry, Repo, RepoPermissions, RepoSettings, Secret } from './types'; +import { + Build, + BuildFeed, + BuildLog, + BuildProc, + Registry, + Repo, + RepoList, + RepoPermissions, + RepoSettings, + Secret, +} from './types'; type RepoListOptions = { all?: boolean; flush?: boolean; }; export default class WoodpeckerClient extends ApiClient { - getRepoList(opts?: RepoListOptions): Promise { + getRepoList(opts?: RepoListOptions): Promise { const query = encodeQueryString(opts); - return this._get(`/api/user/repos?${query}`) as Promise; + return this._get(`/api/user/repos?${query}`) as Promise; } getRepo(owner: string, repo: string): Promise { diff --git a/web/src/lib/api/types/repo.ts b/web/src/lib/api/types/repo.ts index 41db5b09ea..ee06621994 100644 --- a/web/src/lib/api/types/repo.ts +++ b/web/src/lib/api/types/repo.ts @@ -70,3 +70,11 @@ export type RepoPermissions = { admin: boolean; synced: number; }; + +export type RepoList = { + message: string | null; + // error message if any errors occurred + + repos: Repo[]; + // list of the fetched repositories +}; diff --git a/web/src/store/repos.ts b/web/src/store/repos.ts index 9c9f194bf8..a3cb403cf5 100644 --- a/web/src/store/repos.ts +++ b/web/src/store/repos.ts @@ -34,8 +34,8 @@ export default defineStore({ this.repos[repoSlug(repo)] = repo; }, async loadRepos() { - const repos = await apiClient.getRepoList(); - repos.forEach((repo) => { + const result = await apiClient.getRepoList(); + result.repos.forEach((repo) => { this.repos[repoSlug(repo.owner, repo.name)] = repo; }); }, diff --git a/web/src/views/RepoAdd.vue b/web/src/views/RepoAdd.vue index 4348b3fb6f..5bf673552f 100644 --- a/web/src/views/RepoAdd.vue +++ b/web/src/views/RepoAdd.vue @@ -72,13 +72,25 @@ export default defineComponent({ const { searchedRepos } = useRepoSearch(repos, search); onMounted(async () => { - repos.value = await apiClient.getRepoList({ all: true }); + const result = await apiClient.getRepoList({ all: true }); + repos.value = result.repos; + + if (result.message) { + notifications.notify({ title: result.message, type: 'error' }); + } }); const { doSubmit: reloadRepos, isLoading: isReloadingRepos } = useAsyncAction(async () => { + const result = await apiClient.getRepoList({ all: true, flush: true }); + repos.value = undefined; - repos.value = await apiClient.getRepoList({ all: true, flush: true }); - notifications.notify({ title: 'Repository list reloaded', type: 'success' }); + repos.value = result.repos; + + if (result.message) { + notifications.notify({ title: result.message, type: 'error' }); + } else { + notifications.notify({ title: 'Repository list reloaded', type: 'success' }); + } }); const { doSubmit: activateRepo, isLoading: isActivatingRepo } = useAsyncAction(async (repo: Repo) => { diff --git a/woodpecker-go/woodpecker/client.go b/woodpecker-go/woodpecker/client.go index b89f298fb6..e97b5e8e29 100644 --- a/woodpecker-go/woodpecker/client.go +++ b/woodpecker-go/woodpecker/client.go @@ -124,19 +124,19 @@ func (c *client) Repo(owner string, name string) (*Repo, error) { // RepoList returns a list of all repositories to which // the user has explicit access in the host system. func (c *client) RepoList() ([]*Repo, error) { - var out []*Repo + var out RepoList uri := fmt.Sprintf(pathRepos, c.addr) err := c.get(uri, &out) - return out, err + return out.Repos, err } // RepoListOpts returns a list of all repositories to which // the user has explicit access in the host system. func (c *client) RepoListOpts(sync, all bool) ([]*Repo, error) { - var out []*Repo + var out RepoList uri := fmt.Sprintf(pathRepos+"?flush=%v&all=%v", c.addr, sync, all) err := c.get(uri, &out) - return out, err + return out.Repos, err } // RepoPost activates a repository. diff --git a/woodpecker-go/woodpecker/types.go b/woodpecker-go/woodpecker/types.go index ff1ab6cfb7..542686bd46 100644 --- a/woodpecker-go/woodpecker/types.go +++ b/woodpecker-go/woodpecker/types.go @@ -43,6 +43,12 @@ type ( BuildCounter *int `json:"build_counter,omitempty"` } + // RepoList defines the response type of the repos request + RepoList struct { + Message string `json:"message"` + Repos []*Repo `json:"repos"` + } + // Build defines a build object. Build struct { ID int64 `json:"id"`