From fecb12ef9c34ab4bd99e651560e98f01ad8c7baa Mon Sep 17 00:00:00 2001 From: Oliver Braun Date: Thu, 30 Apr 2026 23:24:52 +0200 Subject: [PATCH] feat: add push command to handle deferred branches - Implemented `push` command to push a specified deferred branch to student/group repositories. - Enhanced `GetAssignmentConfig` to include deferred branches configuration. - Updated `Show` method to display deferred branches in assignment configuration. - Created `Push` method in the GitLab client to manage pushing of deferred branches. - Refactored starter repository handling to a new `SourceRepo` type. - Updated related tests and documentation to reflect new functionality. --- cmd/push.go | 37 +++++ config/assignment.go | 37 +++++ config/show.go | 14 ++ config/types.go | 9 ++ docs/commands.md | 39 ++++- git/clone.go | 6 +- git/clone_helpers_test.go | 6 +- git/push.go | 304 ++++++++++++++++++++++++++++++++++++++ git/starterrepo.go | 25 +--- gitlab/generate.go | 10 +- gitlab/push.go | 49 ++++++ gitlab/starterrepo.go | 2 +- gitlab/update.go | 10 +- go.mod | 6 +- go.sum | 20 ++- 15 files changed, 530 insertions(+), 44 deletions(-) create mode 100644 cmd/push.go create mode 100644 git/push.go create mode 100644 gitlab/push.go diff --git a/cmd/push.go b/cmd/push.go new file mode 100644 index 0000000..ec7400b --- /dev/null +++ b/cmd/push.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "fmt" + + "github.com/logrusorgru/aurora/v4" + "github.com/obcode/glabs/v2/config" + "github.com/obcode/glabs/v2/gitlab" + "github.com/spf13/cobra" +) + +var pushCmd = &cobra.Command{ + Use: "push course assignment branch [groups...|students...]", + Short: "Push one deferred branch to student/group repos", + Long: `Push one deferred branch to student/group repos. + You can specify students or groups in order to push only for these. + You cannot push all deferred branches at once.`, + Args: cobra.MinimumNArgs(3), //nolint:gomnd + Run: func(cmd *cobra.Command, args []string) { + course := args[0] + assignment := args[1] + branchname := args[2] + assignmentConfig := config.GetAssignmentConfig(course, assignment, args[3:]...) + assignmentConfig.Show() + fmt.Println(aurora.Magenta("Config okay? Press 'Enter' to continue or 'Ctrl-C' to stop ...")) + fmt.Scanln() //nolint:errcheck + c := gitlab.NewClient() + err := c.Push(assignmentConfig, branchname) + if err != nil { + fmt.Printf("error: %s", err.Error()) + } + }, +} + +func init() { + rootCmd.AddCommand(pushCmd) +} diff --git a/config/assignment.go b/config/assignment.go index 4e17c5b..7612c3e 100644 --- a/config/assignment.go +++ b/config/assignment.go @@ -39,6 +39,42 @@ func GetAssignmentConfig(course, assignment string, onlyForStudentsOrGroups ...s branchRules := branches(assignmentKey, starter) defaultCloneBranch := defaultBranch(branchRules, "main") + deferredBranches := make(map[string]*DeferredBranch) + deferredBranchesCfg := viper.GetStringMap(assignmentKey + ".deferredBranches") + if len(deferredBranchesCfg) > 0 { + for name := range deferredBranchesCfg { + configMap := viper.GetStringMapString(assignmentKey + ".deferredBranches." + name) + + url, ok := configMap["url"] + if !ok { + url = starter.URL + } + fromBranch := configMap["frombranch"] + toBranch, ok := configMap["tobranch"] + if !ok { + toBranch = fromBranch + } + orphanValue, ok := configMap["orphan"] + orphan := true + if ok && orphanValue == "false" { + orphan = false + } + + orphanMessage, ok := configMap["orphanmessage"] + if !ok { + orphanMessage = fmt.Sprintf("Snapshot of %s", name) + } + + deferredBranches[name] = &DeferredBranch{ + URL: url, + FromBranch: fromBranch, + ToBranch: toBranch, + Orphan: orphan, + OrphanMessage: orphanMessage, + } + } + } + assignmentConfig := &AssignmentConfig{ Course: course, Name: assignment, @@ -64,6 +100,7 @@ func GetAssignmentConfig(course, assignment string, onlyForStudentsOrGroups ...s Clone: clone(assignmentKey, defaultCloneBranch), Release: release, Seeder: seeder(assignmentKey), + DeferredBranches: deferredBranches, } return assignmentConfig diff --git a/config/show.go b/config/show.go index fcbb747..1ccd260 100644 --- a/config/show.go +++ b/config/show.go @@ -216,6 +216,20 @@ func (cfg *AssignmentConfig) Show() { } } + writeSectionHeader("DeferredBranches") + if len(cfg.DeferredBranches) == 0 { + writeSectionNotDefined() + } else { + for name, branch := range cfg.DeferredBranches { + writeIndentedHeader(2, fmt.Sprintf("- %s", name)) + writeSectionField(" URL", branch.URL) + writeSectionField(" FromBranch", branch.FromBranch) + writeSectionField(" ToBranch", branch.ToBranch) + writeSectionField(" Orphan", branch.Orphan) + writeSectionField(" OrphanMessage", branch.OrphanMessage) + } + } + writeSectionHeader("MergeRequest") writeSectionField("MergeMethod", mergeMethod) writeSectionField("SquashOption", squashOption) diff --git a/config/types.go b/config/types.go index 275bf3e..a1fe0d3 100644 --- a/config/types.go +++ b/config/types.go @@ -44,6 +44,15 @@ type AssignmentConfig struct { Clone *Clone Release *Release Seeder *Seeder + DeferredBranches map[string]*DeferredBranch +} + +type DeferredBranch struct { + URL string + FromBranch string + ToBranch string + Orphan bool + OrphanMessage string } type Per string diff --git a/docs/commands.md b/docs/commands.md index 1325068..9431084 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -159,6 +159,44 @@ Print glabs version. glabs version ``` +## push + +You can push one deferred branch at a time to all student/group repositories using the `push` command. You can define more than one deferred branch. + +### Assignment Config Example + +```yaml +deferredBranches: + solution: + url: # (optional) source repo, defaults to startercode URL + fromBranch: # (default: solution) + toBranch: # (default: solution) + orphan: # (default: true) + orphanMessage: # (default: Snapshot of solution) + anotherbranch: + url: # (optional) source repo, defaults to startercode URL + fromBranch: # (default: anotherbranch) + toBranch: # (default: anotherbranch) + orphan: # (default: true) + orphanMessage: # (default: Snapshot of anotherbranch) +``` + +- If `orphan: true`, a new orphan branch is created in each repo with a single commit from the deferred branch. +- If `orphan: false`, the deferred branch is pushed as a normal branch (with complete history). +- deferred branches are always pushed with `--force` + +**Usage:** + +```sh +glabs push [groups...|students...] +``` + +e.g. + +```sh +glabs push mpd ass1 solution +``` + ## Filtering students or groups When specifying `[groups...|students...]`, patterns are treated as regular expressions: @@ -209,4 +247,3 @@ glabs generate --help # Show generate help with flags glabs protect --help # Show protect help glabs report --help # Show report help glabs -v generate mpd blatt01 # Run with verbose logging -``` diff --git a/git/clone.go b/git/clone.go index 377105a..e7414c9 100644 --- a/git/clone.go +++ b/git/clone.go @@ -26,16 +26,16 @@ func Clone(cfg *config.AssignmentConfig, noSpinner bool) { case config.PerStudent: for _, stud := range cfg.Students { suffix := cfg.RepoSuffix(stud) - clone(localpath(cfg, suffix), cfg.Clone.Branch, cloneurl(cfg, suffix), auth, cfg.Clone.Force, noSpinner) + clone(localpath(cfg, suffix), cfg.Clone.Branch, ProjectRepoUrl(cfg, suffix), auth, cfg.Clone.Force, noSpinner) } case config.PerGroup: for _, grp := range cfg.Groups { - clone(localpath(cfg, grp.Name), cfg.Clone.Branch, cloneurl(cfg, grp.Name), auth, cfg.Clone.Force, noSpinner) + clone(localpath(cfg, grp.Name), cfg.Clone.Branch, ProjectRepoUrl(cfg, grp.Name), auth, cfg.Clone.Force, noSpinner) } } } -func cloneurl(cfg *config.AssignmentConfig, suffix string) string { +func ProjectRepoUrl(cfg *config.AssignmentConfig, suffix string) string { return fmt.Sprintf("%s/%s-%s", strings.Replace(strings.Replace(cfg.URL, "https://", "git@", 1), "/", ":", 1), cfg.RepoBaseName(), suffix) diff --git a/git/clone_helpers_test.go b/git/clone_helpers_test.go index f074b50..46ee792 100644 --- a/git/clone_helpers_test.go +++ b/git/clone_helpers_test.go @@ -15,7 +15,7 @@ func TestCloneurl_HTTPS(t *testing.T) { UseCoursenameAsPrefix: true, } - got := cloneurl(cfg, "alice") + got := ProjectRepoUrl(cfg, "alice") want := "git@gitlab.example.org:mpd/ss26/blatt-01/mpd-blatt01-alice" if got != want { t.Fatalf("cloneurl() = %q, want %q", got, want) @@ -30,7 +30,7 @@ func TestCloneurl_ContainsExpectedParts(t *testing.T) { UseCoursenameAsPrefix: true, } - got := cloneurl(cfg, "bob") + got := ProjectRepoUrl(cfg, "bob") if strings.Contains(got, "https://") { t.Fatalf("cloneurl() should not contain https://, got %q", got) @@ -51,7 +51,7 @@ func TestCloneurl_WithoutCoursePrefix(t *testing.T) { UseCoursenameAsPrefix: false, } - got := cloneurl(cfg, "team1") + got := ProjectRepoUrl(cfg, "team1") want := "git@gitlab.example.org:mpd/ss26/blatt-01/blatt01-team1" if got != want { t.Fatalf("cloneurl() = %q, want %q", got, want) diff --git a/git/push.go b/git/push.go new file mode 100644 index 0000000..305f7f3 --- /dev/null +++ b/git/push.go @@ -0,0 +1,304 @@ +package git + +import ( + "fmt" + "time" + + "github.com/go-git/go-billy/v5/memfs" + git "github.com/go-git/go-git/v5" + gitconfig "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/storage/memory" + "github.com/logrusorgru/aurora" + "github.com/obcode/glabs/v2/config" + "github.com/rs/zerolog/log" + "github.com/theckman/yacspin" + gitlab "gitlab.com/gitlab-org/api/client-go/v2" +) + +func CloneBranch(url, fromBranch string, orphan bool, orphanMessage string) (*git.Repository, plumbing.ReferenceName, error) { + cfg := yacspin.Config{ + Frequency: 100 * time.Millisecond, + CharSet: yacspin.CharSets[69], + Suffix: aurora.Sprintf(aurora.Cyan(" cloning source code from %s, branch %s"), + aurora.Yellow(url), + aurora.Yellow(fromBranch), + ), + SuffixAutoColon: true, + StopCharacter: "✓", + StopColors: []string{"fgGreen"}, + StopFailMessage: "error", + StopFailCharacter: "✗", + StopFailColors: []string{"fgRed"}, + } + + spinner, err := yacspin.New(cfg) + if err != nil { + log.Debug().Err(err).Msg("cannot create spinner") + } + err = spinner.Start() + if err != nil { + log.Debug().Err(err).Msg("cannot start spinner") + } + + auth, err := GetAuth() + if err != nil { + fmt.Printf("error: %v", err) + return nil, "", err + } + + storer := memory.NewStorage() + fs := memfs.New() + + sourceRef := plumbing.NewBranchReferenceName(fromBranch) + + repo, err := git.Clone(storer, fs, &git.CloneOptions{ + URL: url, + ReferenceName: sourceRef, + SingleBranch: true, + Auth: auth, + }) + if err != nil { + spinner.StopFailMessage(fmt.Sprintf("problem: %v", err)) + + err := spinner.StopFail() + if err != nil { + log.Debug().Err(err).Msg("cannot stop spinner") + } + return nil, "", err + } + + wt, err := repo.Worktree() + if err != nil { + spinner.StopFailMessage(fmt.Sprintf("problem: %v", err)) + + err := spinner.StopFail() + if err != nil { + log.Debug().Err(err).Msg("cannot stop spinner") + } + return nil, "", err + } + + if err := wt.Checkout(&git.CheckoutOptions{ + Branch: sourceRef, + Force: true, + }); err != nil { + spinner.StopFailMessage(fmt.Sprintf("problem: %v", err)) + + err := spinner.StopFail() + if err != nil { + log.Debug().Err(err).Msg("cannot stop spinner") + } + return nil, "", err + } + + if !orphan { + return repo, sourceRef, nil + } + + headRef, err := repo.Head() + if err != nil { + spinner.StopFailMessage(fmt.Sprintf("problem: %v", err)) + + err := spinner.StopFail() + if err != nil { + log.Debug().Err(err).Msg("cannot stop spinner") + } + return nil, "", err + } + + headCommit, err := repo.CommitObject(headRef.Hash()) + if err != nil { + spinner.StopFailMessage(fmt.Sprintf("problem: %v", err)) + + err := spinner.StopFail() + if err != nil { + log.Debug().Err(err).Msg("cannot stop spinner") + } + return nil, "", err + } + + tree, err := headCommit.Tree() + if err != nil { + spinner.StopFailMessage(fmt.Sprintf("problem: %v", err)) + + err := spinner.StopFail() + if err != nil { + log.Debug().Err(err).Msg("cannot stop spinner") + } + return nil, "", err + } + + orphanBranchName := fmt.Sprintf("orphan-%s-%d", fromBranch, time.Now().UnixNano()) + orphanRef := plumbing.NewBranchReferenceName(orphanBranchName) + + now := time.Now() + commit := &object.Commit{ + Author: object.Signature{ + Name: "glabs", + Email: "noreply@example.com", + When: now, + }, + Committer: object.Signature{ + Name: "glabs", + Email: "noreply@example.com", + When: now, + }, + Message: orphanMessage, + TreeHash: tree.Hash, + } + + encoded := repo.Storer.NewEncodedObject() + if err := commit.Encode(encoded); err != nil { + spinner.StopFailMessage(fmt.Sprintf("problem: %v", err)) + + err := spinner.StopFail() + if err != nil { + log.Debug().Err(err).Msg("cannot stop spinner") + } + return nil, "", err + } + + commitHash, err := repo.Storer.SetEncodedObject(encoded) + if err != nil { + spinner.StopFailMessage(fmt.Sprintf("problem: %v", err)) + + err := spinner.StopFail() + if err != nil { + log.Debug().Err(err).Msg("cannot stop spinner") + } + return nil, "", err + } + + if err := repo.Storer.SetReference(plumbing.NewHashReference(orphanRef, commitHash)); err != nil { + spinner.StopFailMessage(fmt.Sprintf("problem: %v", err)) + + err := spinner.StopFail() + if err != nil { + log.Debug().Err(err).Msg("cannot stop spinner") + } + return nil, "", err + } + + if err := repo.CreateBranch(&gitconfig.Branch{ + Name: orphanRef.Short(), + Merge: orphanRef, + }); err != nil && err != git.ErrBranchExists { + spinner.StopFailMessage(fmt.Sprintf("problem: %v", err)) + + err := spinner.StopFail() + if err != nil { + log.Debug().Err(err).Msg("cannot stop spinner") + } + return nil, "", err + } + + if err := wt.Checkout(&git.CheckoutOptions{ + Branch: orphanRef, + Force: true, + }); err != nil { + spinner.StopFailMessage(fmt.Sprintf("problem: %v", err)) + + err := spinner.StopFail() + if err != nil { + log.Debug().Err(err).Msg("cannot stop spinner") + } + return nil, "", err + } + + if orphan { + spinner.StopMessage(fmt.Sprintf("using branch '%s' with single commit '%s'", orphanRef.Short(), orphanMessage)) + } + errs := spinner.Stop() + if errs != nil { + log.Debug().Err(err).Msg("cannot stop spinner") + } + + return repo, orphanRef, nil +} + +func PushBranch(assignmentCfg *config.AssignmentConfig, projectname string, repo *git.Repository, localRef plumbing.ReferenceName, toBranch string, force bool, project *gitlab.Project) error { + cfg := yacspin.Config{ + Frequency: 100 * time.Millisecond, + CharSet: yacspin.CharSets[69], + Suffix: aurora.Sprintf(aurora.Cyan(" pushing branch %s to project %s / branch %s"), + aurora.Yellow(localRef.Short()), + aurora.Magenta(assignmentCfg.URL+"/"+project.Name), + aurora.Magenta(toBranch), + ), + SuffixAutoColon: true, + StopCharacter: "✓", + StopColors: []string{"fgGreen"}, + StopFailMessage: "error", + StopFailCharacter: "✗", + StopFailColors: []string{"fgRed"}, + } + + spinner, err := yacspin.New(cfg) + if err != nil { + log.Debug().Err(err).Msg("cannot create spinner") + } + err = spinner.Start() + if err != nil { + log.Debug().Err(err).Msg("cannot start spinner") + } + + conf := &gitconfig.RemoteConfig{ + Name: project.Name, + URLs: []string{project.SSHURLToRepo}, + } + + remote, err := repo.CreateRemote(conf) + if err != nil { + spinner.StopFailMessage(fmt.Sprintf("problem: %v", err)) + + err := spinner.StopFail() + if err != nil { + log.Debug().Err(err).Msg("cannot stop spinner") + } + log.Debug().Err(err). + Str("name", project.Name).Str("url", project.SSHURLToRepo). + Msg("cannot create remote") + return fmt.Errorf("cannot create remote: %w", err) + } + + auth, err := GetAuth() + if err != nil { + spinner.StopFailMessage(fmt.Sprintf("problem: %v", err)) + + err := spinner.StopFail() + if err != nil { + log.Debug().Err(err).Msg("cannot stop spinner") + } + fmt.Printf("error: %v", err) + return err + } + + spec := localRef.String() + ":" + plumbing.NewBranchReferenceName(toBranch).String() + if force { + spec = "+" + spec + } + + pushOpts := &git.PushOptions{ + RemoteName: remote.Config().Name, + RefSpecs: []gitconfig.RefSpec{gitconfig.RefSpec(spec)}, + Auth: auth, + } + + err = repo.Push(pushOpts) + if err != nil { + spinner.StopFailMessage(fmt.Sprintf("problem: %v", err)) + + err := spinner.StopFail() + if err != nil { + log.Debug().Err(err).Msg("cannot stop spinner") + } + return err + } + err = spinner.Stop() + if err != nil { + log.Debug().Err(err).Msg("cannot stop spinner") + } + return nil +} diff --git a/git/starterrepo.go b/git/starterrepo.go index a49a0a8..31b86f9 100644 --- a/git/starterrepo.go +++ b/git/starterrepo.go @@ -9,31 +9,22 @@ import ( "github.com/go-git/go-git/v5/plumbing/transport/ssh" "github.com/go-git/go-git/v5/storage/memory" "github.com/logrusorgru/aurora" - "github.com/obcode/glabs/v2/config" "github.com/rs/zerolog/log" "github.com/theckman/yacspin" ) -type Starterrepo struct { +type SourceRepo struct { Repo *git.Repository Auth ssh.AuthMethod } -func PrepareStartercodeRepo(assignmentCfg *config.AssignmentConfig) (*Starterrepo, error) { - if assignmentCfg.Startercode == nil { - log.Debug(). - Str("course", assignmentCfg.Course). - Str("assignment", assignmentCfg.Name). - Msg("no startercode provided") - return nil, nil - } - +func PrepareSourceRepo(url, fromBranch string) (*SourceRepo, error) { cfg := yacspin.Config{ Frequency: 100 * time.Millisecond, CharSet: yacspin.CharSets[69], - Suffix: aurora.Sprintf(aurora.Cyan(" cloning startercode from %s, branch %s"), - aurora.Yellow(assignmentCfg.Startercode.URL), - aurora.Yellow(assignmentCfg.Startercode.FromBranch), + Suffix: aurora.Sprintf(aurora.Cyan(" cloning source code from %s, branch %s"), + aurora.Yellow(url), + aurora.Yellow(fromBranch), ), SuffixAutoColon: true, StopCharacter: "✓", @@ -65,8 +56,8 @@ func PrepareStartercodeRepo(assignmentCfg *config.AssignmentConfig) (*Starterrep r, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{ Auth: auth, - URL: assignmentCfg.Startercode.URL, - ReferenceName: plumbing.ReferenceName("refs/heads/" + assignmentCfg.Startercode.FromBranch), + URL: url, + ReferenceName: plumbing.ReferenceName("refs/heads/" + fromBranch), }) errs := spinner.Stop() @@ -78,7 +69,7 @@ func PrepareStartercodeRepo(assignmentCfg *config.AssignmentConfig) (*Starterrep return nil, fmt.Errorf("error while cloning repo (wrong URL or no rights?): %w", err) } - return &Starterrepo{ + return &SourceRepo{ Repo: r, Auth: auth, }, nil diff --git a/gitlab/generate.go b/gitlab/generate.go index 36d0f1c..86b11a7 100644 --- a/gitlab/generate.go +++ b/gitlab/generate.go @@ -26,10 +26,10 @@ func (c *Client) Generate(assignmentCfg *config.AssignmentConfig) { } } - var starterrepo *git.Starterrepo + var starterrepo *git.SourceRepo if assignmentCfg.Startercode != nil { - starterrepo, err = git.PrepareStartercodeRepo(assignmentCfg) + starterrepo, err = git.PrepareSourceRepo(assignmentCfg.Startercode.URL, assignmentCfg.Startercode.FromBranch) if err != nil { fmt.Println(err) @@ -49,7 +49,7 @@ func (c *Client) Generate(assignmentCfg *config.AssignmentConfig) { } func (c *Client) generate(assignmentCfg *config.AssignmentConfig, assignmentGroupID int64, - projectname string, members []*config.Student, starterrepo *git.Starterrepo) { + projectname string, members []*config.Student, starterrepo *git.SourceRepo) { cfg := yacspin.Config{ Frequency: 100 * time.Millisecond, @@ -225,7 +225,7 @@ func (c *Client) generate(assignmentCfg *config.AssignmentConfig, assignmentGrou } func (c *Client) generatePerStudent(assignmentCfg *config.AssignmentConfig, assignmentGroupID int64, - starterrepo *git.Starterrepo) { + starterrepo *git.SourceRepo) { if len(assignmentCfg.Students) == 0 { log.Info().Str("group", assignmentCfg.Course).Msg("no students found") return @@ -238,7 +238,7 @@ func (c *Client) generatePerStudent(assignmentCfg *config.AssignmentConfig, assi } func (c *Client) generatePerGroup(assignmentCfg *config.AssignmentConfig, assignmentGroupID int64, - starterrepo *git.Starterrepo) { + starterrepo *git.SourceRepo) { if len(assignmentCfg.Groups) == 0 { log.Info().Str("group", assignmentCfg.Course).Msg("no groups found") return diff --git a/gitlab/push.go b/gitlab/push.go new file mode 100644 index 0000000..0f97b57 --- /dev/null +++ b/gitlab/push.go @@ -0,0 +1,49 @@ +package gitlab + +import ( + "fmt" + + "github.com/obcode/glabs/v2/config" + "github.com/obcode/glabs/v2/git" +) + +func (c *Client) Push(assignmentCfg *config.AssignmentConfig, branchname string) error { + branch, ok := assignmentCfg.DeferredBranches[branchname] + if !ok { + return fmt.Errorf("error: no config for deferred branch \"%s\" found\n", branchname) + } + + repo, branchRef, err := git.CloneBranch(branch.URL, branch.FromBranch, branch.Orphan, branch.OrphanMessage) + if err != nil { + return err + } + + names := make([]string, 0) + + switch assignmentCfg.Per { + case config.PerStudent: + for _, student := range assignmentCfg.Students { + names = append(names, assignmentCfg.RepoNameForStudent(student)) + } + case config.PerGroup: + for _, grp := range assignmentCfg.Groups { + names = append(names, assignmentCfg.RepoNameForGroup(grp)) + } + } + + for _, name := range names { + projectname := assignmentCfg.Path + "/" + name + + project, err := c.getProjectByName(projectname) + if err != nil { + return err + } + + err = git.PushBranch(assignmentCfg, projectname, repo, branchRef, branch.ToBranch, true, project) + if err != nil { + return err + } + } + + return nil +} diff --git a/gitlab/starterrepo.go b/gitlab/starterrepo.go index 9214b50..aeb8264 100644 --- a/gitlab/starterrepo.go +++ b/gitlab/starterrepo.go @@ -11,7 +11,7 @@ import ( gitlab "gitlab.com/gitlab-org/api/client-go/v2" ) -func (c *Client) pushStartercode(assignmentCfg *cfg.AssignmentConfig, from *g.Starterrepo, project *gitlab.Project) error { +func (c *Client) pushStartercode(assignmentCfg *cfg.AssignmentConfig, from *g.SourceRepo, project *gitlab.Project) error { conf := &config.RemoteConfig{ Name: project.Name, URLs: []string{project.SSHURLToRepo}, diff --git a/gitlab/update.go b/gitlab/update.go index 9be6bca..9600316 100644 --- a/gitlab/update.go +++ b/gitlab/update.go @@ -19,10 +19,10 @@ func (c *Client) Update(assignmentCfg *config.AssignmentConfig) { exitFunc(1) } - var starterrepo *git.Starterrepo + var starterrepo *git.SourceRepo if assignmentCfg.Startercode != nil { - starterrepo, err = git.PrepareStartercodeRepo(assignmentCfg) + starterrepo, err = git.PrepareSourceRepo(assignmentCfg.Startercode.URL, assignmentCfg.Startercode.FromBranch) if err != nil { fmt.Println(err) @@ -41,7 +41,7 @@ func (c *Client) Update(assignmentCfg *config.AssignmentConfig) { } } -func (c *Client) update(assignmentCfg *config.AssignmentConfig, project *gitlab.Project, starterrepo *git.Starterrepo) { +func (c *Client) update(assignmentCfg *config.AssignmentConfig, project *gitlab.Project, starterrepo *git.SourceRepo) { cfg := yacspin.Config{ Frequency: 100 * time.Millisecond, @@ -88,7 +88,7 @@ func (c *Client) update(assignmentCfg *config.AssignmentConfig, project *gitlab. } } -func (c *Client) updatePerStudent(assignmentCfg *config.AssignmentConfig, starterrepo *git.Starterrepo) { +func (c *Client) updatePerStudent(assignmentCfg *config.AssignmentConfig, starterrepo *git.SourceRepo) { if len(assignmentCfg.Students) == 0 { log.Info().Str("group", assignmentCfg.Course).Msg("no students found") return @@ -108,7 +108,7 @@ func (c *Client) updatePerStudent(assignmentCfg *config.AssignmentConfig, starte } } -func (c *Client) updatePerGroup(assignmentCfg *config.AssignmentConfig, starterrepo *git.Starterrepo) { +func (c *Client) updatePerGroup(assignmentCfg *config.AssignmentConfig, starterrepo *git.SourceRepo) { if len(assignmentCfg.Groups) == 0 { log.Info().Str("group", assignmentCfg.Course).Msg("no groups found") return diff --git a/go.mod b/go.mod index fa7e4c7..14dd800 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,9 @@ go 1.26 require ( github.com/ProtonMail/go-crypto v1.4.1 + github.com/go-git/go-billy/v5 v5.8.0 github.com/go-git/go-git/v5 v5.18.0 + github.com/go-viper/mapstructure/v2 v2.4.0 github.com/gookit/color v1.6.0 github.com/logrusorgru/aurora v2.0.3+incompatible github.com/logrusorgru/aurora/v4 v4.0.0 @@ -12,6 +14,7 @@ require ( github.com/rs/zerolog v1.35.1 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 + github.com/testcontainers/testcontainers-go v0.42.0 github.com/theckman/yacspin v0.13.12 gitlab.com/gitlab-org/api/client-go/v2 v2.20.1 golang.org/x/term v0.42.0 @@ -40,11 +43,9 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.8.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/go-querystring v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect @@ -86,7 +87,6 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/testify v1.11.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/testcontainers/testcontainers-go v0.42.0 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect diff --git a/go.sum b/go.sum index 6da9da0..aedbb2c 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= @@ -30,6 +30,8 @@ github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7np github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -193,6 +195,8 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= 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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -223,20 +227,20 @@ go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= +go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE= golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= @@ -273,3 +277,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=