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
10 changes: 10 additions & 0 deletions config/assignment.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ func GetAssignmentConfig(course, assignment string, onlyForStudentsOrGroups ...s
Msg("configuration for assignment not found")
}

// Abstract assignments are bases for `extends` only and must not be used
// directly. Checked before resolving inheritance so the (own) flag is read,
// not an inherited one.
if assignmentIsAbstract(course, assignment) {
log.Fatal().
Str("course", course).
Str("assignment", assignment).
Msg("assignment is abstract (a base for 'extends') and cannot be used directly")
}

// Resolve `extends` inheritance before reading any fields so the rest of
// the config loading sees the merged, effective configuration.
resolveAssignmentInheritance(course, assignment)
Expand Down
15 changes: 15 additions & 0 deletions config/inheritance.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@ import (
// assignmentpath: blatt-10 # ... and override only what differs
const inheritKey = "extends"

// abstractKey marks an assignment as an abstract base that exists only to be
// inherited from via `extends`. Abstract assignments cannot be operated on
// directly (generate, protect, clone, …). The flag itself is never inherited.
const abstractKey = "abstract"

// assignmentIsAbstract reports whether the assignment declares `abstract: true`
// on itself. It reads the assignment's own value and must be called before
// resolveAssignmentInheritance writes the merged config back into viper, so an
// inherited abstract flag never makes a concrete child abstract.
func assignmentIsAbstract(course, assignment string) bool {
return viper.GetBool(course + "." + assignment + "." + abstractKey)
}

// resolveAssignmentInheritance resolves the `extends` chain for the given
// assignment and writes the merged, effective configuration back into viper at
// the assignment key. After this call the rest of the config loading reads the
Expand All @@ -35,7 +48,9 @@ func resolveAssignmentInheritance(course, assignment string) {
}

merged := mergedAssignmentMap(course, assignment, map[string]bool{})
// Meta keys must never leak into the effective config or be inherited.
delete(merged, inheritKey)
delete(merged, abstractKey)
viper.Set(assignmentKey, merged)
}

Expand Down
36 changes: 36 additions & 0 deletions config/inheritance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,42 @@ func TestInheritance_NoExtendsIsUnaffected(t *testing.T) {
}
}

func TestAssignmentIsAbstract(t *testing.T) {
baseAssignment(t)

// blatt09 (from baseAssignment) is concrete.
if assignmentIsAbstract("mpd", "blatt09") {
t.Fatal("blatt09 should not be abstract")
}

// A declared abstract base.
viper.Set("mpd.defaults", true)
viper.Set("mpd.defaults.abstract", true)
viper.Set("mpd.defaults.per", "student")
if !assignmentIsAbstract("mpd", "defaults") {
t.Fatal("defaults should be abstract")
}

// A child extending an abstract base must NOT itself become abstract:
// the flag is read from the own value before inheritance is resolved.
viper.Set("mpd.blatt10", true)
viper.Set("mpd.blatt10.extends", "defaults")
viper.Set("mpd.blatt10.assignmentpath", "blatt-10")
if assignmentIsAbstract("mpd", "blatt10") {
t.Fatal("blatt10 extends an abstract base but must not be abstract itself")
}

// And after full resolution the abstract flag must not leak into the
// effective config.
cfg := GetAssignmentConfig("mpd", "blatt10")
if cfg.Per != PerStudent {
t.Fatalf("Per = %q, want inherited student", cfg.Per)
}
if viper.GetBool("mpd.blatt10.abstract") {
t.Fatal("abstract flag leaked into resolved blatt10 config")
}
}

func TestDeepMerge(t *testing.T) {
parent := map[string]any{
"a": 1,
Expand Down
31 changes: 31 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,37 @@ Notes:
- Inheritance chains are allowed (`blatt11` → `blatt10` → `blatt09`); values are
resolved transitively. Cycles and missing parents are reported as errors.

#### Abstract base assignments

A common pattern is a base entry that holds shared configuration and is only ever
inherited from, never generated. Mark it with `abstract: true` — much like an
abstract class. Any command targeting it directly (`generate`, `protect`,
`clone`, …) is rejected with an error.

```yaml
defaults:
abstract: true # base only — `glabs generate mpd defaults` is refused
per: student
mergeRequest:
mergeMethod: semi_linear
squashOption: never
pipeline: true
branches:
- name: main
mergeOnly: true

blatt01:
extends: defaults # inherits everything above
assignmentpath: blatt-01
description: Blatt 1
startercode:
url: git@gitlab.lrz.de:mpd/startercode/blatt-01.git
fromBranch: template
```

The `abstract` flag is **not inherited**: `blatt01` above stays a normal,
generatable assignment.

### Access level values

| Level | Value | Permissions |
Expand Down
Loading