Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions gitlab/repos_overview.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package gitlab

import (
"strconv"

"github.com/obcode/glabs/v3/config"
gitlab "gitlab.com/gitlab-org/api/client-go/v2"
)

// ExistingRepoNames lists the names of the projects that actually exist in an
// assignment's GitLab group — one paginated group listing rather than a GetProject
// per student, so a whole-course overview stays cheap (one call per assignment, not
// per repository). The caller matches these against RepoTargets to see which repos
// have been generated. A missing group (assignment never generated) surfaces as an
// error the caller can treat as "nothing generated".
func (c *Client) ExistingRepoNames(assignmentCfg *config.AssignmentConfig) (map[string]bool, error) {
groupID, err := c.getGroupID(assignmentCfg)
if err != nil {
return nil, err
}

names := make(map[string]bool)
opts := &gitlab.ListGroupProjectsOptions{ListOptions: gitlab.ListOptions{PerPage: 100}}
for {
projects, resp, err := c.Groups.ListGroupProjects(groupID, opts)
if err != nil {
return nil, err
}
for _, p := range projects {
names[p.Name] = true
}
next := nextPageFromHeader(resp)
if next == 0 {
break
}
opts.Page = int64(next)
}
return names, nil
}

// nextPageFromHeader reads GitLab's X-Next-Page pagination header (0 = last page).
func nextPageFromHeader(resp *gitlab.Response) int {
if resp == nil {
return 0
}
vals := resp.Header["X-Next-Page"]
if len(vals) == 0 || vals[0] == "" {
return 0
}
n, err := strconv.Atoi(vals[0])
if err != nil {
return 0
}
return n
}
111 changes: 111 additions & 0 deletions web/app/repos_overview.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package app

import (
"context"
"sort"
"sync"

"github.com/obcode/glabs/v3/config"
"github.com/obcode/glabs/v3/reporter"
)

// repoOverviewConcurrency bounds how many assignments are queried at once.
const repoOverviewConcurrency = 6

// RepoStatus is one target repository and whether it actually exists in GitLab.
type RepoStatus struct {
For string
Repo string
URL string
Exists bool
}

// AssignmentRepos summarises, for one assignment, how many of its target repos have
// been generated. Note carries a reason when the assignment could not be checked
// (abstract/unresolvable, or its GitLab group does not exist yet).
type AssignmentRepos struct {
Name string
Per string
Targets int
Existing int
Repos []RepoStatus
Note string
}

// CourseRepoOverview reports, per assignment of one of the caller's courses, which
// target repositories actually exist in GitLab — so the owner can see at a glance
// what has been generated and who is missing. It uses one group listing per
// assignment (not a lookup per student) and queries assignments concurrently. It
// needs the caller's stored GitLab token.
func (a *App) CourseRepoOverview(ctx context.Context, course string) ([]*AssignmentRepos, error) {
o, err := owner(ctx)
if err != nil {
return nil, err
}
stored, err := a.Course(ctx, course)
if err != nil {
return nil, err
}
client, err := a.gitlabClientFor(ctx, o, reporter.NewDiscardReporter())
if err != nil {
return nil, err
}

names := make([]string, 0, len(stored.Source.Assignments))
for name := range stored.Source.Assignments {
names = append(names, name)
}
sort.Strings(names)

out := make([]*AssignmentRepos, len(names))
sem := make(chan struct{}, repoOverviewConcurrency)
var wg sync.WaitGroup
for i, name := range names {
wg.Add(1)
go func(i int, name string) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()

ar := &AssignmentRepos{Name: name}
cfg, err := a.resolveAssignmentConfig(ctx, course, name)
if err != nil || cfg == nil {
ar.Note = "abstrakt oder nicht auflösbar — übersprungen"
out[i] = ar
return
}
ar.Per = string(cfg.Per)
targets := cfg.RepoTargets()
ar.Targets = len(targets)

existing, err := client.ExistingRepoNames(cfg)
if err != nil {
// Most likely the group does not exist yet (never generated); show
// every target as missing rather than failing the whole overview.
ar.Note = "GitLab-Gruppe nicht gefunden — vermutlich noch nicht generiert"
ar.Repos, ar.Existing = matchRepos(targets, nil)
out[i] = ar
return
}
ar.Repos, ar.Existing = matchRepos(targets, existing)
out[i] = ar
}(i, name)
}
wg.Wait()
return out, nil
}

// matchRepos pairs each target with whether its repo exists, and counts the
// existing ones. A nil existing set means none exist.
func matchRepos(targets []config.RepoTarget, existing map[string]bool) ([]RepoStatus, int) {
repos := make([]RepoStatus, 0, len(targets))
count := 0
for _, t := range targets {
ex := existing[t.Repo]
if ex {
count++
}
repos = append(repos, RepoStatus{For: t.For, Repo: t.Repo, URL: t.URL, Exists: ex})
}
return repos, count
}
44 changes: 44 additions & 0 deletions web/app/repos_overview_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package app

import (
"testing"

"github.com/obcode/glabs/v3/config"
)

func TestMatchRepos(t *testing.T) {
targets := []config.RepoTarget{
{For: "a@hm.edu", Repo: "c-b01-a", URL: "https://gl/c-b01-a"},
{For: "b@hm.edu", Repo: "c-b01-b", URL: "https://gl/c-b01-b"},
{For: "c@hm.edu", Repo: "c-b01-c", URL: "https://gl/c-b01-c"},
}
existing := map[string]bool{"c-b01-a": true, "c-b01-c": true} // b is missing

repos, count := matchRepos(targets, existing)
if count != 2 {
t.Errorf("existing count = %d, want 2", count)
}
if len(repos) != 3 {
t.Fatalf("repos = %d, want 3", len(repos))
}
got := map[string]bool{}
for _, r := range repos {
got[r.For] = r.Exists
}
if !got["a@hm.edu"] || got["b@hm.edu"] || !got["c@hm.edu"] {
t.Errorf("exists flags wrong: %+v", got)
}
}

func TestMatchRepos_nilExistingMarksAllMissing(t *testing.T) {
targets := []config.RepoTarget{{For: "a@hm.edu", Repo: "r-a"}, {For: "b@hm.edu", Repo: "r-b"}}
repos, count := matchRepos(targets, nil)
if count != 0 {
t.Errorf("count = %d, want 0 for a nil existing set", count)
}
for _, r := range repos {
if r.Exists {
t.Errorf("%s should be missing when nothing exists", r.For)
}
}
}
Loading
Loading