diff --git a/models/bots/runner.go b/models/bots/runner.go index f09f89d8ea..dd97b92866 100644 --- a/models/bots/runner.go +++ b/models/bots/runner.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/timeutil" + "xorm.io/builder" ) @@ -40,7 +41,7 @@ type Runner struct { } func (Runner) TableName() string { - return "actions_runner" + return "bots_runner" } func init() { diff --git a/models/bots/task.go b/models/bots/task.go index 65eb9de77b..73a9da867c 100644 --- a/models/bots/task.go +++ b/models/bots/task.go @@ -5,16 +5,26 @@ package bots import ( + "context" "errors" "fmt" "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "github.com/google/uuid" + "xorm.io/builder" ) +func init() { + db.RegisterModel(new(Task)) + db.RegisterModel(new(BuildIndex)) +} + // TaskStatus represents a task status type TaskStatus int @@ -31,27 +41,70 @@ const ( // Task represnets bot tasks type Task struct { - ID int64 - UUID string `xorm:"CHAR(36)"` - RepoID int64 `xorm:"index"` - TriggerUserID int64 - Ref string - CommitSHA string - Event webhook.HookEventType - Token string // token for this task - Grant string // permissions for this task - EventPayload string `xorm:"LONGTEXT"` - RunnerID int64 `xorm:"index"` - Status TaskStatus `xorm:"index"` - Created timeutil.TimeStamp `xorm:"created"` - StartTime timeutil.TimeStamp - EndTime timeutil.TimeStamp - Updated timeutil.TimeStamp `xorm:"updated"` + ID int64 + Title string + UUID string `xorm:"CHAR(36)"` + Index int64 `xorm:"index unique(repo_index)"` + RepoID int64 `xorm:"index unique(repo_index)"` + TriggerUserID int64 + TriggerUser *user_model.User `xorm:"-"` + Ref string + CommitSHA string + Event webhook.HookEventType + Token string // token for this task + Grant string // permissions for this task + EventPayload string `xorm:"LONGTEXT"` + RunnerID int64 `xorm:"index"` + Status TaskStatus `xorm:"index"` + WorkflowsStatuses map[string]map[string]TaskStatus `xorm:"LONGTEXT"` + Created timeutil.TimeStamp `xorm:"created"` + StartTime timeutil.TimeStamp + EndTime timeutil.TimeStamp + Updated timeutil.TimeStamp `xorm:"updated"` +} + +func (t *Task) IsPending() bool { + return t.Status == TaskPending || t.Status == TaskAssigned +} + +func (t *Task) IsRunning() bool { + return t.Status == TaskRunning +} + +func (t *Task) IsFailed() bool { + return t.Status == TaskFailed || t.Status == TaskCanceled || t.Status == TaskTimeout +} + +func (t *Task) IsSuccess() bool { + return t.Status == TaskFinished } // TableName represents a bot task func (Task) TableName() string { - return "actions_task" + return "bots_task" +} + +func (t *Task) HTMLURL() string { + return fmt.Sprintf("") +} + +func updateRepoBuildsNumbers(ctx context.Context, repo *repo_model.Repository) error { + _, err := db.GetEngine(ctx).ID(repo.ID). + SetExpr("num_builds", + builder.Select("count(*)").From("bots_task"). + Where(builder.Eq{"repo_id": repo.ID}), + ). + SetExpr("num_closed_builds", + builder.Select("count(*)").From("bots_task"). + Where(builder.Eq{ + "repo_id": repo.ID, + }.And( + builder.In("status", TaskFailed, TaskCanceled, TaskTimeout, TaskFinished), + ), + ), + ). + Update(repo) + return err } // InsertTask inserts a bot task @@ -59,7 +112,27 @@ func InsertTask(t *Task) error { if t.UUID == "" { t.UUID = uuid.New().String() } - return db.Insert(db.DefaultContext, t) + index, err := db.GetNextResourceIndex("build_index", t.RepoID) + if err != nil { + return err + } + t.Index = index + + ctx, commiter, err := db.TxContext() + if err != nil { + return err + } + defer commiter.Close() + + if err := db.Insert(ctx, t); err != nil { + return err + } + + if err := updateRepoBuildsNumbers(ctx, &repo_model.Repository{ID: t.RepoID}); err != nil { + return err + } + + return commiter.Commit() } // UpdateTask updates bot task @@ -70,7 +143,9 @@ func UpdateTask(t *Task, cols ...string) error { // ErrTaskNotExist represents an error for bot task not exist type ErrTaskNotExist struct { - UUID string + RepoID int64 + Index int64 + UUID string } func (err ErrTaskNotExist) Error() string { @@ -91,8 +166,8 @@ func GetTaskByUUID(taskUUID string) (*Task, error) { return &task, nil } -// GetCurTask return the task for the bot -func GetCurTask(runnerID int64) (*Task, error) { +// GetCurTaskByID return the task for the bot +func GetCurTaskByID(runnerID int64) (*Task, error) { var tasks []Task // FIXME: for test, just return all tasks err := db.GetEngine(db.DefaultContext).Where("status=?", TaskPending).Find(&tasks) @@ -108,6 +183,31 @@ func GetCurTask(runnerID int64) (*Task, error) { return &tasks[0], err } +// GetCurTaskByUUID return the task for the bot +func GetCurTaskByUUID(runnerUUID string) (*Task, error) { + runner, err := GetRunnerByUUID(runnerUUID) + if err != nil { + return nil, err + } + return GetCurTaskByID(runner.ID) +} + +func GetTaskByRepoAndIndex(repoID, index int64) (*Task, error) { + var task Task + has, err := db.GetEngine(db.DefaultContext).Where("repo_id=?", repoID). + And("`index` = ?", index). + Get(&task) + if err != nil { + return nil, err + } else if !has { + return nil, ErrTaskNotExist{ + RepoID: repoID, + Index: index, + } + } + return &task, nil +} + // AssignTaskToRunner assign a task to a runner func AssignTaskToRunner(taskID int64, runnerID int64) error { cnt, err := db.GetEngine(db.DefaultContext). @@ -126,6 +226,41 @@ func AssignTaskToRunner(taskID int64, runnerID int64) error { return nil } +type FindTaskOptions struct { + db.ListOptions + RepoID int64 + IsClosed util.OptionalBool +} + +func (opts FindTaskOptions) toConds() builder.Cond { + cond := builder.NewCond() + if opts.RepoID > 0 { + cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + } + if opts.IsClosed.IsTrue() { + cond = cond.And(builder.Expr("status IN (?,?,?,?)", TaskCanceled, TaskFailed, TaskTimeout, TaskFinished)) + } else if opts.IsClosed.IsFalse() { + cond = cond.And(builder.Expr("status IN (?,?,?)", TaskPending, TaskAssigned, TaskRunning)) + } + return cond +} + +func FindTasks(opts FindTaskOptions) (TaskList, error) { + sess := db.GetEngine(db.DefaultContext).Where(opts.toConds()) + if opts.ListOptions.PageSize > 0 { + skip, take := opts.GetSkipTake() + sess.Limit(take, skip) + } + var tasks []*Task + return tasks, sess.Find(&tasks) +} + +func CountTasks(opts FindTaskOptions) (int64, error) { + return db.GetEngine(db.DefaultContext).Table("bots_task").Where(opts.toConds()).Count() +} + type TaskStage struct{} type StageStep struct{} + +type BuildIndex db.ResourceIndex diff --git a/models/bots/task_list.go b/models/bots/task_list.go new file mode 100644 index 0000000000..351e334f7d --- /dev/null +++ b/models/bots/task_list.go @@ -0,0 +1,37 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package bots + +import ( + "code.gitea.io/gitea/models/db" + user_model "code.gitea.io/gitea/models/user" +) + +type TaskList []*Task + +// GetUserIDs returns a slice of user's id +func (tasks TaskList) GetUserIDs() []int64 { + userIDsMap := make(map[int64]struct{}) + for _, task := range tasks { + userIDsMap[task.TriggerUserID] = struct{}{} + } + userIDs := make([]int64, 0, len(userIDsMap)) + for userID := range userIDsMap { + userIDs = append(userIDs, userID) + } + return userIDs +} + +func (tasks TaskList) LoadTriggerUser() error { + userIDs := tasks.GetUserIDs() + users := make(map[int64]*user_model.User, len(userIDs)) + if err := db.GetEngine(db.DefaultContext).In("id", userIDs).Find(&users); err != nil { + return err + } + for _, task := range tasks { + task.TriggerUser = users[task.TriggerUserID] + } + return nil +} diff --git a/models/migrations/v216.go b/models/migrations/v216.go index 114cc6c4c7..fd0d700574 100644 --- a/models/migrations/v216.go +++ b/models/migrations/v216.go @@ -5,13 +5,14 @@ package migrations import ( + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/timeutil" "xorm.io/xorm" ) func addBotTables(x *xorm.Engine) error { - type BotRunner struct { + type BotsRunner struct { ID int64 UUID string `xorm:"CHAR(36) UNIQUE"` Name string `xorm:"VARCHAR(32) UNIQUE"` @@ -24,29 +25,37 @@ func addBotTables(x *xorm.Engine) error { Base int // 0 native 1 docker 2 virtual machine RepoRange string // glob match which repositories could use this runner Token string - LastOnline timeutil.TimeStamp + LastOnline timeutil.TimeStamp `xorm:"index"` Created timeutil.TimeStamp `xorm:"created"` } - type BotTask struct { - ID int64 - UUID string `xorm:"CHAR(36)"` - RepoID int64 `xorm:"index"` - Type string `xorm:"VARCHAR(16)"` // 0 commit 1 pullrequest - Ref string - CommitSHA string - Event string - Token string // token for this task - Grant string // permissions for this task - EventPayload string `xorm:"LONGTEXT"` - RunnerID int64 `xorm:"index"` - Status int - Content string `xorm:"LONGTEXT"` - Created timeutil.TimeStamp `xorm:"created"` - StartTime timeutil.TimeStamp - EndTime timeutil.TimeStamp - Updated timeutil.TimeStamp `xorm:"updated"` + type BotsTask struct { + ID int64 + Title string + UUID string `xorm:"CHAR(36)"` + Index int64 `xorm:"index unique(repo_index)"` + RepoID int64 `xorm:"index unique(repo_index)"` + TriggerUserID int64 + Ref string + CommitSHA string + Event string + Token string // token for this task + Grant string // permissions for this task + EventPayload string `xorm:"LONGTEXT"` + RunnerID int64 `xorm:"index"` + Status int `xorm:"index"` + Created timeutil.TimeStamp `xorm:"created"` + StartTime timeutil.TimeStamp + EndTime timeutil.TimeStamp + Updated timeutil.TimeStamp `xorm:"updated"` } - return x.Sync2(new(BotRunner), new(BotTask)) + type Repository struct { + NumBuilds int `xorm:"NOT NULL DEFAULT 0"` + NumClosedBuilds int `xorm:"NOT NULL DEFAULT 0"` + } + + type BuildIndex db.ResourceIndex + + return x.Sync2(new(BotsRunner), new(BotsTask), new(Repository), new(BuildIndex)) } diff --git a/models/repo/repo.go b/models/repo/repo.go index a3dac8383f..37d8cffd60 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -142,6 +142,9 @@ type Repository struct { NumProjects int `xorm:"NOT NULL DEFAULT 0"` NumClosedProjects int `xorm:"NOT NULL DEFAULT 0"` NumOpenProjects int `xorm:"-"` + NumBuilds int `xorm:"NOT NULL DEFAULT 0"` + NumClosedBuilds int `xorm:"NOT NULL DEFAULT 0"` + NumOpenBuilds int `xorm:"-"` IsPrivate bool `xorm:"INDEX"` IsEmpty bool `xorm:"INDEX"` @@ -234,6 +237,7 @@ func (repo *Repository) AfterLoad() { repo.NumOpenPulls = repo.NumPulls - repo.NumClosedPulls repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones repo.NumOpenProjects = repo.NumProjects - repo.NumClosedProjects + repo.NumOpenBuilds = repo.NumBuilds - repo.NumClosedBuilds } // LoadAttributes loads attributes of the repository. diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index c21c1e7a79..c4090255cb 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -175,7 +175,7 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { r.Config = new(PullRequestsConfig) case unit.TypeIssues: r.Config = new(IssuesConfig) - case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects, unit.TypePackages: + case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypeProjects, unit.TypePackages, unit.TypeBuilds: fallthrough default: r.Config = new(UnitConfig) diff --git a/models/unit/unit.go b/models/unit/unit.go index b83bd61831..672494be98 100644 --- a/models/unit/unit.go +++ b/models/unit/unit.go @@ -28,6 +28,7 @@ const ( TypeExternalTracker // 7 ExternalTracker TypeProjects // 8 Kanban board TypePackages // 9 Packages + TypeBuilds // 10 Builds ) // Value returns integer value for unit type @@ -55,6 +56,8 @@ func (u Type) String() string { return "TypeProjects" case TypePackages: return "TypePackages" + case TypeBuilds: + return "TypeBuilds" } return fmt.Sprintf("Unknown Type %d", u) } @@ -78,6 +81,7 @@ var ( TypeExternalTracker, TypeProjects, TypePackages, + TypeBuilds, } // DefaultRepoUnits contains the default unit types @@ -289,6 +293,15 @@ var ( perm.AccessModeRead, } + UnitBuilds = Unit{ + TypeBuilds, + "repo.builds", + "/builds", + "repo.builds.desc", + 7, + perm.AccessModeOwner, + } + // Units contains all the units Units = map[Type]Unit{ TypeCode: UnitCode, @@ -300,6 +313,7 @@ var ( TypeExternalWiki: UnitExternalWiki, TypeProjects: UnitProjects, TypePackages: UnitPackages, + TypeBuilds: UnitBuilds, } ) diff --git a/modules/context/context.go b/modules/context/context.go index 47368bb280..78d3472b67 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -807,6 +807,7 @@ func Contexter(ctx context.Context) func(next http.Handler) http.Handler { ctx.Data["UnitIssuesGlobalDisabled"] = unit.TypeIssues.UnitGlobalDisabled() ctx.Data["UnitPullsGlobalDisabled"] = unit.TypePullRequests.UnitGlobalDisabled() ctx.Data["UnitProjectsGlobalDisabled"] = unit.TypeProjects.UnitGlobalDisabled() + ctx.Data["UnitBuildsGlobalDisabled"] = unit.TypeBuilds.UnitGlobalDisabled() ctx.Data["locale"] = locale ctx.Data["AllLangs"] = translation.AllLangs() diff --git a/modules/context/repo.go b/modules/context/repo.go index 1a83c49e95..5408991d72 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -1043,6 +1043,7 @@ func UnitTypes() func(ctx *Context) { ctx.Data["UnitTypeExternalTracker"] = unit_model.TypeExternalTracker ctx.Data["UnitTypeProjects"] = unit_model.TypeProjects ctx.Data["UnitTypePackages"] = unit_model.TypePackages + ctx.Data["UnitTypeBuilds"] = unit_model.TypeBuilds } } diff --git a/modules/notification/bots/bots.go b/modules/notification/bots/bots.go index 8fc31ed3cf..a6dec9f449 100644 --- a/modules/notification/bots/bots.go +++ b/modules/notification/bots/bots.go @@ -39,6 +39,28 @@ func NewNotifier() base.Notifier { return &botsNotifier{} } +func detectWorkflows(commit *git.Commit, event webhook.HookEventType, ref string) (bool, error) { + tree, err := commit.SubTree(".github/workflows") + if _, ok := err.(git.ErrNotExist); ok { + tree, err = commit.SubTree(".gitea/workflows") + } + if _, ok := err.(git.ErrNotExist); ok { + return false, nil + } + if err != nil { + return false, err + } + + entries, err := tree.ListEntries() + if err != nil { + return false, err + } + + log.Trace("detected %s has %d entries", commit.ID, len(entries)) + + return len(entries) > 0, nil +} + func notifyIssue(issue *models.Issue, doer *user_model.User, evt webhook.HookEventType, payload string) { err := issue.LoadRepo(db.DefaultContext) if err != nil { @@ -68,7 +90,18 @@ func notifyIssue(issue *models.Issue, doer *user_model.User, evt webhook.HookEve return } + hasWorkflows, err := detectWorkflows(commit, evt, ref) + if err != nil { + log.Error("detectWorkflows: %v", err) + return + } + if !hasWorkflows { + log.Trace("repo %s with commit %s couldn't find workflows", issue.Repo.RepoPath(), commit.ID) + return + } + task := bots_model.Task{ + Title: commit.CommitMessage, RepoID: issue.RepoID, TriggerUserID: doer.ID, Event: evt, @@ -157,6 +190,29 @@ func (a *botsNotifier) NotifyPushCommits(pusher *user_model.User, repo *repo_mod return } + gitRepo, err := git.OpenRepository(ctx, repo.RepoPath()) + if err != nil { + log.Error("commits.ToAPIPayloadCommits failed: %v", err) + return + } + defer gitRepo.Close() + + commit, err := gitRepo.GetCommit(commits.HeadCommit.Sha1) + if err != nil { + log.Error("commits.ToAPIPayloadCommits failed: %v", err) + return + } + + hasWorkflows, err := detectWorkflows(commit, webhook.HookEventPush, opts.RefFullName) + if err != nil { + log.Error("detectWorkflows: %v", err) + return + } + if !hasWorkflows { + log.Trace("repo %s with commit %s couldn't find workflows", repo.RepoPath(), commit.ID) + return + } + payload := &api.PushPayload{ Ref: opts.RefFullName, Before: opts.OldCommitID, @@ -176,6 +232,7 @@ func (a *botsNotifier) NotifyPushCommits(pusher *user_model.User, repo *repo_mod } task := bots_model.Task{ + Title: commit.Message(), RepoID: repo.ID, TriggerUserID: pusher.ID, Event: webhook.HookEventPush, diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 8503cb78d7..7cfaac0efd 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1219,6 +1219,10 @@ projects.open = Open projects.close = Close projects.board.assigned_to = Assigned to +builds = Builds +builds.desc = Manage builds +builds.opened_by = opened %[1]s by %[2]s + issues.desc = Organize bug reports, tasks and milestones. issues.filter_assignees = Filter Assignee issues.filter_milestones = Filter Milestone diff --git a/routers/api/bots/bots.go b/routers/api/bots/bots.go index 0e91c19617..32fdc75673 100644 --- a/routers/api/bots/bots.go +++ b/routers/api/bots/bots.go @@ -37,11 +37,13 @@ var upgrader = websocket.Upgrader{ var pongWait = 60 * time.Second type Message struct { - Version int // - Type int // message type, 1 register 2 error - RunnerUUID string // runner uuid - ErrCode int // error code - ErrContent string // errors message + Version int // + Type int // message type, 1 register 2 error 3 task 4 no task + RunnerUUID string // runner uuid + ErrCode int // error code + ErrContent string // errors message + EventName string + EventPayload string } func Serve(w http.ResponseWriter, r *http.Request) { @@ -112,7 +114,7 @@ MESSAGE_BUMP: Version: 1, Type: 2, ErrCode: 1, - ErrContent: "type is not supported", + ErrContent: fmt.Sprintf("message type %d is not supported", msg.Type), } bs, err := json.Marshal(&returnMsg) if err != nil { @@ -145,5 +147,42 @@ MESSAGE_BUMP: } // TODO: find new task and send to client + task, err := bots_model.GetCurTaskByUUID(msg.RunnerUUID) + if err != nil { + log.Error("websocket[%s] get task failed: %v", r.RemoteAddr, err) + break + } + if task == nil { + returnMsg := Message{ + Version: 1, + Type: 4, + } + bs, err := json.Marshal(&returnMsg) + if err != nil { + log.Error("websocket[%s] marshal message failed: %v", r.RemoteAddr, err) + break MESSAGE_BUMP + } + err = c.WriteMessage(mt, bs) + if err != nil { + log.Error("websocket[%s] sent message failed: %v", r.RemoteAddr, err) + } + } else { + returnMsg := Message{ + Version: 1, + Type: 3, + EventName: task.Event.Event(), + EventPayload: task.EventPayload, + } + bs, err := json.Marshal(&returnMsg) + if err != nil { + log.Error("websocket[%s] marshal message failed: %v", r.RemoteAddr, err) + break + } + err = c.WriteMessage(mt, bs) + if err != nil { + log.Error("websocket[%s] sent message failed: %v", r.RemoteAddr, err) + } + } + } } diff --git a/routers/web/repo/builds/builds.go b/routers/web/repo/builds/builds.go new file mode 100644 index 0000000000..f7d8be4a58 --- /dev/null +++ b/routers/web/repo/builds/builds.go @@ -0,0 +1,99 @@ +// Copyright 2018 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package builds + +import ( + "net/http" + + bots_model "code.gitea.io/gitea/models/bots" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" + "code.gitea.io/gitea/modules/util" +) + +const ( + tplListBuilds base.TplName = "repo/builds/list" + tplViewBuild base.TplName = "repo/builds/view" +) + +// MustEnableBuilds check if builds are enabled in settings +func MustEnableBuilds(ctx *context.Context) { + if unit.TypeBuilds.UnitGlobalDisabled() { + ctx.NotFound("EnableTypeBuilds", nil) + return + } + + if ctx.Repo.Repository != nil { + if !ctx.Repo.CanRead(unit.TypeBuilds) { + ctx.NotFound("MustEnableBuilds", nil) + return + } + } +} + +func List(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("repo.builds") + ctx.Data["PageIsBuildList"] = true + + page := ctx.FormInt("page") + if page <= 0 { + page = 1 + } + + opts := bots_model.FindTaskOptions{ + ListOptions: db.ListOptions{ + Page: page, + PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), + }, + RepoID: ctx.Repo.Repository.ID, + } + if ctx.FormString("state") == "closed" { + opts.IsClosed = util.OptionalBoolTrue + } else { + opts.IsClosed = util.OptionalBoolFalse + } + tasks, err := bots_model.FindTasks(opts) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + + if err := tasks.LoadTriggerUser(); err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + + total, err := bots_model.CountTasks(opts) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + + ctx.Data["Tasks"] = tasks + + pager := context.NewPagination(int(total), opts.PageSize, opts.Page, 5) + pager.SetDefaultParams(ctx) + ctx.Data["Page"] = pager + + ctx.HTML(http.StatusOK, tplListBuilds) +} + +func ViewBuild(ctx *context.Context) { + index := ctx.ParamsInt64("index") + task, err := bots_model.GetTaskByRepoAndIndex(ctx.Repo.Repository.ID, index) + if err != nil { + ctx.Error(http.StatusInternalServerError, err.Error()) + return + } + + ctx.Data["Title"] = task.Title + " - " + ctx.Tr("repo.builds") + ctx.Data["PageIsBuildList"] = true + ctx.Data["Build"] = task + + ctx.HTML(http.StatusOK, tplViewBuild) +} diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go index 2b5691ce88..69088e1821 100644 --- a/routers/web/repo/setting.go +++ b/routers/web/repo/setting.go @@ -489,6 +489,15 @@ func SettingsPost(ctx *context.Context) { deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePackages) } + if form.EnableBuilds && !unit_model.TypeBuilds.UnitGlobalDisabled() { + units = append(units, repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeBuilds, + }) + } else if !unit_model.TypeBuilds.UnitGlobalDisabled() { + deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeBuilds) + } + if form.EnablePulls && !unit_model.TypePullRequests.UnitGlobalDisabled() { units = append(units, repo_model.RepoUnit{ RepoID: repo.ID, diff --git a/routers/web/web.go b/routers/web/web.go index fe5007abb7..2dff1d3544 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -35,6 +35,7 @@ import ( "code.gitea.io/gitea/routers/web/misc" "code.gitea.io/gitea/routers/web/org" "code.gitea.io/gitea/routers/web/repo" + "code.gitea.io/gitea/routers/web/repo/builds" "code.gitea.io/gitea/routers/web/user" user_setting "code.gitea.io/gitea/routers/web/user/setting" "code.gitea.io/gitea/routers/web/user/setting/security" @@ -665,6 +666,7 @@ func RegisterRoutes(m *web.Route) { reqRepoIssuesOrPullsReader := context.RequireRepoReaderOr(unit.TypeIssues, unit.TypePullRequests) reqRepoProjectsReader := context.RequireRepoReader(unit.TypeProjects) reqRepoProjectsWriter := context.RequireRepoWriter(unit.TypeProjects) + reqRepoBuildsReader := context.RequireRepoReader(unit.TypeBuilds) reqPackageAccess := func(accessMode perm.AccessMode) func(ctx *context.Context) { return func(ctx *context.Context) { @@ -1169,6 +1171,13 @@ func RegisterRoutes(m *web.Route) { }, reqRepoProjectsWriter, context.RepoMustNotBeArchived()) }, reqRepoProjectsReader, repo.MustEnableProjects) + m.Group("/builds", func() { + m.Get("", builds.List) + m.Group("/{index}", func() { + m.Get("", builds.ViewBuild) + }) + }, reqRepoBuildsReader, builds.MustEnableBuilds) + m.Group("/wiki", func() { m.Combo("/"). Get(repo.Wiki). diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 64f47aadd5..6652178cf8 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -148,6 +148,7 @@ type RepoSettingForm struct { EnableProjects bool EnablePackages bool EnablePulls bool + EnableBuilds bool PullsIgnoreWhitespace bool PullsAllowMerge bool PullsAllowRebase bool diff --git a/templates/repo/builds/build_list.tmpl b/templates/repo/builds/build_list.tmpl new file mode 100644 index 0000000000..55d177cbcb --- /dev/null +++ b/templates/repo/builds/build_list.tmpl @@ -0,0 +1,38 @@ +