Compare commits

...

4 Commits

Author SHA1 Message Date
jmiller 240fe1ebe5 feat: security scanning API endpoints + pre-receive hook blocking (#692)
PR RC Release / Build RC Release (pull_request) Successful in 2s
Generic: Project CI / Lint & Validate (pull_request) Successful in 37s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Universal: PR Check / Secret Scan (pull_request) Successful in 50s
Universal: Auto Version Bump / Version Bump (push) Successful in 14s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Add REST API for security alerts (list, get, update status, trigger scan)
and scanner config (get, update). Wire block_on_push into the pre-receive
hook so pushes containing detected secrets are rejected with details.

Claude-Session: https://claude.ai/code/session_011AAFzotGMf3ayvXhEmStCd
2026-06-28 02:14:46 -05:00
jmiller ecc1f20162 feat: cascade merge — auto-create PRs to downstream branches after merge (#460)
Adds configurable cascade rules per repo. When a PR merges into a
source branch, the system auto-creates PRs to each configured target
branch. Skips if a matching PR already exists.

- Model: CascadeMergeRule (repo_id, source, target, enabled, auto_merge)
- Migration v362 creates cascade_merge_rule table
- Notifier hooks into MergePullRequest/AutoMergePullRequest events
- API: CRUD at /repos/{owner}/{repo}/cascade_rules (admin only)

Claude-Session: https://claude.ai/code/session_011AAFzotGMf3ayvXhEmStCd
2026-06-28 02:04:40 -05:00
jmiller 965abb54b8 feat: add issue status presets and cross-org migration (#507)
4 built-in presets: default, software-development, support-tickets,
bug-tracking. API endpoints to list presets, apply to org, and copy
statuses between orgs. Web UI dropdown on org settings page.

Claude-Session: https://claude.ai/code/session_011AAFzotGMf3ayvXhEmStCd
2026-06-28 02:03:15 -05:00
jmiller b94f41b597 feat(orgs): auto-create default teams on org creation (#513)
New organizations now get three default teams in addition to Owners:
- Developers (write: code, issues, PRs, wiki, projects; read: releases)
- Reviewers (read: code, issues, PRs, releases, wiki)
- CI/CD (write: actions, packages, releases; read: code)

Teams are defined in DefaultOrgTeams and created inside the same
transaction as the org, so creation is atomic.

Claude-Session: https://claude.ai/code/session_011AAFzotGMf3ayvXhEmStCd
2026-06-28 02:00:17 -05:00
20 changed files with 1126 additions and 1 deletions
+6
View File
@@ -3,6 +3,10 @@
## [Unreleased]
### Added
- Cascade merge: auto-create PRs to downstream branches after merge with configurable rules per repo (#460)
- Issue status presets: 4 built-in templates (default, software-development, support-tickets, bug-tracking) with API + web UI (#507)
- Cross-org status migration: copy status definitions from one org to another via API (#507)
- Auto-create default teams on org creation: Developers (write), Reviewers (read), CI/CD (actions+packages) (#513)
- Branch protection delete allowlist: configurable per-user/team/deploy-key allowlist for deleting protected branches (#696)
- Workflow subdirectory discovery: workflows in subdirectories of `.mokogitea/workflows/` are now auto-discovered (#693)
- API token scope `read:licensing` / `write:licensing` for licensing endpoints (#697)
@@ -10,6 +14,8 @@
- Wiki full-text search: case-insensitive search across all wiki page titles and content (#550)
- Wiki search API: GET /wiki/search?q=term with paginated JSON results (#550)
- Metadata deploy fields: deploy_host, deploy_port, deploy_user, deploy_path, docker_image, docker_registry, container_name, health_url (#692)
- Security scanning API: REST endpoints for alerts, config, and on-demand scans (GET/PATCH /security/alerts, /security/config, POST /security/scan) (#692)
- Pre-receive hook secret blocking: push rejection when block_on_push enabled and secrets detected in commits (#692)
- Metadata API partial updates: PUT /metadata now merges only sent fields instead of replacing all
- Wiki revision diff: line-by-line diff view per commit in wiki page history (#667)
- Wiki categories: YAML frontmatter `categories:` with category index page (#668)
+205
View File
@@ -33,6 +33,211 @@ func (IssueStatusDef) TableName() string {
return "issue_status_def"
}
// ──────────────────────────────────────────────────────────────────────
// Presets
// ──────────────────────────────────────────────────────────────────────
// StatusPresetEntry defines a single status in a preset template.
type StatusPresetEntry struct {
Name string
Color string
Description string
ClosesIssue bool
IsRequired bool
}
// StatusPreset defines a named collection of status definitions.
type StatusPreset struct {
Name string
Description string
Statuses []StatusPresetEntry
}
// StatusPresets is the registry of built-in status presets.
var StatusPresets = map[string]*StatusPreset{
"default": {
Name: "default",
Description: "General-purpose workflow (default seed)",
Statuses: []StatusPresetEntry{
{Name: "Open", Color: "#2563eb", Description: "New or active issue", ClosesIssue: false, IsRequired: true},
{Name: "In Progress", Color: "#7c3aed", Description: "Work is actively being done"},
{Name: "Waiting", Color: "#f59e0b", Description: "Blocked or waiting for input"},
{Name: "In Review", Color: "#0891b2", Description: "PR submitted, awaiting review"},
{Name: "Closed", Color: "#16a34a", Description: "Completed or resolved", ClosesIssue: true, IsRequired: true},
{Name: "Won't Fix", Color: "#6b7280", Description: "Decided not to address", ClosesIssue: true},
},
},
"software-development": {
Name: "software-development",
Description: "Software development lifecycle",
Statuses: []StatusPresetEntry{
{Name: "Open", Color: "#2563eb", Description: "New or active issue", IsRequired: true},
{Name: "In Progress", Color: "#7c3aed", Description: "Developer is working on this"},
{Name: "In Review", Color: "#0891b2", Description: "Pull request submitted, awaiting review"},
{Name: "Testing", Color: "#d97706", Description: "Being tested or in QA"},
{Name: "Closed", Color: "#16a34a", Description: "Completed, merged, and deployed", ClosesIssue: true, IsRequired: true},
{Name: "Won't Fix", Color: "#6b7280", Description: "Decided not to address", ClosesIssue: true},
},
},
"support-tickets": {
Name: "support-tickets",
Description: "Customer support ticket workflow",
Statuses: []StatusPresetEntry{
{Name: "New", Color: "#2563eb", Description: "Ticket received, not yet triaged", IsRequired: true},
{Name: "Assigned", Color: "#7c3aed", Description: "Assigned to a support agent"},
{Name: "Waiting for Customer", Color: "#f59e0b", Description: "Awaiting customer response"},
{Name: "In Progress", Color: "#0891b2", Description: "Agent is actively working on this"},
{Name: "Resolved", Color: "#16a34a", Description: "Issue resolved, awaiting confirmation", ClosesIssue: true},
{Name: "Closed", Color: "#059669", Description: "Confirmed resolved", ClosesIssue: true, IsRequired: true},
},
},
"bug-tracking": {
Name: "bug-tracking",
Description: "Bug lifecycle tracking",
Statuses: []StatusPresetEntry{
{Name: "New", Color: "#2563eb", Description: "Bug reported, not yet triaged", IsRequired: true},
{Name: "Confirmed", Color: "#dc2626", Description: "Bug confirmed and reproducible"},
{Name: "In Progress", Color: "#7c3aed", Description: "Developer is working on a fix"},
{Name: "Fixed", Color: "#0891b2", Description: "Fix implemented, awaiting verification"},
{Name: "Verified", Color: "#16a34a", Description: "Fix verified by QA"},
{Name: "Closed", Color: "#059669", Description: "Bug resolved and closed", ClosesIssue: true, IsRequired: true},
{Name: "Won't Fix", Color: "#6b7280", Description: "Decided not to fix", ClosesIssue: true},
},
},
}
// StatusPresetNames returns the list of available preset names in display order.
func StatusPresetNames() []string {
return []string{"default", "software-development", "support-tickets", "bug-tracking"}
}
// ApplyStatusPreset replaces all non-required statuses for an org with a preset.
// Required statuses (Open/Closed) are preserved if they already exist; the preset's
// required entries are created if missing. Non-required statuses are soft-deleted
// (is_active=false) and the preset's non-required entries are inserted.
func ApplyStatusPreset(ctx context.Context, orgID int64, presetName string) error {
preset, ok := StatusPresets[presetName]
if !ok {
return db.ErrNotExist{Resource: "StatusPreset", ID: 0}
}
existing, err := GetAllIssueStatusDefsByOrg(ctx, orgID)
if err != nil {
return err
}
// Build lookup of existing statuses by name
existingByName := make(map[string]*IssueStatusDef, len(existing))
for _, d := range existing {
existingByName[d.Name] = d
}
// Deactivate all non-required existing statuses
for _, d := range existing {
if d.IsRequired {
continue
}
if d.IsActive {
d.IsActive = false
if _, err := db.GetEngine(ctx).ID(d.ID).Cols("is_active").Update(d); err != nil {
return err
}
}
}
// Apply preset entries
for i, entry := range preset.Statuses {
if ex, found := existingByName[entry.Name]; found {
// Update existing status to match preset
ex.Color = entry.Color
ex.Description = entry.Description
ex.ClosesIssue = entry.ClosesIssue
ex.SortOrder = i
ex.IsActive = true
if _, err := db.GetEngine(ctx).ID(ex.ID).AllCols().Update(ex); err != nil {
return err
}
} else {
// Create new status
def := &IssueStatusDef{
OrgID: orgID,
Name: entry.Name,
Color: entry.Color,
Description: entry.Description,
ClosesIssue: entry.ClosesIssue,
IsRequired: entry.IsRequired,
SortOrder: i,
IsActive: true,
}
if _, err := db.GetEngine(ctx).Insert(def); err != nil {
return err
}
}
}
return nil
}
// CopyStatusesFromOrg copies all active status definitions from srcOrgID to dstOrgID.
// Existing non-required statuses in dstOrgID are deactivated first.
func CopyStatusesFromOrg(ctx context.Context, srcOrgID, dstOrgID int64) error {
srcDefs, err := GetIssueStatusDefsByOrg(ctx, srcOrgID)
if err != nil {
return err
}
existing, err := GetAllIssueStatusDefsByOrg(ctx, dstOrgID)
if err != nil {
return err
}
existingByName := make(map[string]*IssueStatusDef, len(existing))
for _, d := range existing {
existingByName[d.Name] = d
}
// Deactivate non-required existing statuses
for _, d := range existing {
if d.IsRequired {
continue
}
if d.IsActive {
d.IsActive = false
if _, err := db.GetEngine(ctx).ID(d.ID).Cols("is_active").Update(d); err != nil {
return err
}
}
}
// Copy source statuses
for _, src := range srcDefs {
if ex, found := existingByName[src.Name]; found {
ex.Color = src.Color
ex.Description = src.Description
ex.ClosesIssue = src.ClosesIssue
ex.SortOrder = src.SortOrder
ex.IsActive = true
if _, err := db.GetEngine(ctx).ID(ex.ID).AllCols().Update(ex); err != nil {
return err
}
} else {
def := &IssueStatusDef{
OrgID: dstOrgID,
Name: src.Name,
Color: src.Color,
Description: src.Description,
ClosesIssue: src.ClosesIssue,
IsRequired: src.IsRequired,
SortOrder: src.SortOrder,
IsActive: true,
}
if _, err := db.GetEngine(ctx).Insert(def); err != nil {
return err
}
}
}
return nil
}
// ──────────────────────────────────────────────────────────────────────
// Queries
// ──────────────────────────────────────────────────────────────────────
+1
View File
@@ -438,6 +438,7 @@ func prepareMigrationTasks() []*migration {
newMigration(358, "Add licensing tables (license, entitlement, activation, product_tier)", v1_27.AddLicensingTables),
newMigration(359, "Add deploy fields to repo manifest", v1_27.AddDeployFieldsToRepoManifest),
newMigration(360, "Add delete allowlist to protected branch", v1_27.AddDeleteAllowlistToProtectedBranch),
newMigration(361, "Add cascade merge rule table", v1_27.AddCascadeMergeRuleTable),
}
return preparedMigrations
}
+24
View File
@@ -0,0 +1,24 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
"xorm.io/xorm"
)
func AddCascadeMergeRuleTable(x *xorm.Engine) error {
type CascadeMergeRule struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
SourceBranch string `xorm:"UNIQUE(s) VARCHAR(255) NOT NULL"`
TargetBranch string `xorm:"UNIQUE(s) VARCHAR(255) NOT NULL"`
Enabled bool `xorm:"NOT NULL DEFAULT true"`
AutoMerge bool `xorm:"NOT NULL DEFAULT false"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
return x.Sync(new(CascadeMergeRule))
}
+86 -1
View File
@@ -323,6 +323,60 @@ func (org *Organization) UnitPermission(ctx context.Context, doer *user_model.Us
}
// CreateOrganization creates record of a new organization.
// DefaultTeamSpec defines a team to auto-create when a new organization is created.
type DefaultTeamSpec struct {
Name string
Description string
AccessMode perm.AccessMode
IncludesAllRepositories bool
CanCreateOrgRepo bool
Units map[unit.Type]perm.AccessMode
}
// DefaultOrgTeams is the list of teams created for every new organization
// (in addition to the mandatory Owners team). Override in tests or via init.
var DefaultOrgTeams = []DefaultTeamSpec{
{
Name: "Developers",
Description: "Members with write access to code, issues, and pull requests",
AccessMode: perm.AccessModeWrite,
IncludesAllRepositories: true,
Units: map[unit.Type]perm.AccessMode{
unit.TypeCode: perm.AccessModeWrite,
unit.TypeIssues: perm.AccessModeWrite,
unit.TypePullRequests: perm.AccessModeWrite,
unit.TypeReleases: perm.AccessModeRead,
unit.TypeWiki: perm.AccessModeWrite,
unit.TypeProjects: perm.AccessModeWrite,
},
},
{
Name: "Reviewers",
Description: "Members with read access for code review",
AccessMode: perm.AccessModeRead,
IncludesAllRepositories: true,
Units: map[unit.Type]perm.AccessMode{
unit.TypeCode: perm.AccessModeRead,
unit.TypeIssues: perm.AccessModeRead,
unit.TypePullRequests: perm.AccessModeRead,
unit.TypeReleases: perm.AccessModeRead,
unit.TypeWiki: perm.AccessModeRead,
},
},
{
Name: "CI/CD",
Description: "Members with write access to actions and packages",
AccessMode: perm.AccessModeWrite,
IncludesAllRepositories: true,
Units: map[unit.Type]perm.AccessMode{
unit.TypeCode: perm.AccessModeRead,
unit.TypeActions: perm.AccessModeWrite,
unit.TypePackages: perm.AccessModeWrite,
unit.TypeReleases: perm.AccessModeWrite,
},
},
}
func CreateOrganization(ctx context.Context, org *Organization, owner *user_model.User) (err error) {
if !owner.CanCreateOrganization() {
return ErrUserNotAllowedCreateOrg{}
@@ -348,7 +402,7 @@ func CreateOrganization(ctx context.Context, org *Organization, owner *user_mode
}
org.UseCustomAvatar = true
org.MaxRepoCreation = -1
org.NumTeams = 1
org.NumTeams = 1 + len(DefaultOrgTeams)
org.NumMembers = 1
org.Type = user_model.UserTypeOrganization
@@ -413,6 +467,37 @@ func CreateOrganization(ctx context.Context, org *Organization, owner *user_mode
}); err != nil {
return fmt.Errorf("insert team-user relation: %w", err)
}
for _, spec := range DefaultOrgTeams {
dt := &Team{
OrgID: org.ID,
LowerName: strings.ToLower(spec.Name),
Name: spec.Name,
Description: spec.Description,
AccessMode: spec.AccessMode,
IncludesAllRepositories: spec.IncludesAllRepositories,
CanCreateOrgRepo: spec.CanCreateOrgRepo,
}
if err = db.Insert(ctx, dt); err != nil {
return fmt.Errorf("insert default team %q: %w", spec.Name, err)
}
dtUnits := make([]TeamUnit, 0, len(spec.Units))
for tp, am := range spec.Units {
dtUnits = append(dtUnits, TeamUnit{
OrgID: org.ID,
TeamID: dt.ID,
Type: tp,
AccessMode: am,
})
}
if len(dtUnits) > 0 {
if err = db.Insert(ctx, &dtUnits); err != nil {
return fmt.Errorf("insert default team %q units: %w", spec.Name, err)
}
}
}
return nil
})
}
+63
View File
@@ -0,0 +1,63 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package repo
import (
"context"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
)
type CascadeMergeRule struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
SourceBranch string `xorm:"UNIQUE(s) VARCHAR(255) NOT NULL"`
TargetBranch string `xorm:"UNIQUE(s) VARCHAR(255) NOT NULL"`
Enabled bool `xorm:"NOT NULL DEFAULT true"`
AutoMerge bool `xorm:"NOT NULL DEFAULT false"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
}
func init() {
db.RegisterModel(new(CascadeMergeRule))
}
func GetCascadeRulesByRepoID(ctx context.Context, repoID int64) ([]*CascadeMergeRule, error) {
rules := make([]*CascadeMergeRule, 0)
return rules, db.GetEngine(ctx).Where("repo_id = ?", repoID).Find(&rules)
}
func GetCascadeRulesForBranch(ctx context.Context, repoID int64, sourceBranch string) ([]*CascadeMergeRule, error) {
rules := make([]*CascadeMergeRule, 0)
return rules, db.GetEngine(ctx).Where("repo_id = ? AND source_branch = ? AND enabled = ?", repoID, sourceBranch, true).Find(&rules)
}
func GetCascadeRuleByID(ctx context.Context, id int64) (*CascadeMergeRule, error) {
rule := &CascadeMergeRule{ID: id}
has, err := db.GetEngine(ctx).Get(rule)
if err != nil {
return nil, err
}
if !has {
return nil, nil
}
return rule, nil
}
func CreateCascadeRule(ctx context.Context, rule *CascadeMergeRule) error {
_, err := db.GetEngine(ctx).Insert(rule)
return err
}
func UpdateCascadeRule(ctx context.Context, rule *CascadeMergeRule) error {
_, err := db.GetEngine(ctx).ID(rule.ID).Cols("source_branch", "target_branch", "enabled", "auto_merge").Update(rule)
return err
}
func DeleteCascadeRule(ctx context.Context, repoID, id int64) error {
_, err := db.GetEngine(ctx).Where("id = ? AND repo_id = ?", id, repoID).Delete(&CascadeMergeRule{})
return err
}
+18
View File
@@ -169,6 +169,24 @@ type IssueStatusDef struct {
SortOrder int `json:"sort_order"`
}
// StatusPresetEntry represents a single status in a preset template.
// swagger:model
type StatusPresetEntry struct {
Name string `json:"name"`
Color string `json:"color"`
Description string `json:"description"`
ClosesIssue bool `json:"closes_issue"`
IsRequired bool `json:"is_required"`
}
// StatusPreset represents a named status preset template.
// swagger:model
type StatusPreset struct {
Name string `json:"name"`
Description string `json:"description"`
Statuses []*StatusPresetEntry `json:"statuses"`
}
// IssuePriorityDef represents an org-level issue priority definition
// swagger:model
type IssuePriorityDef struct {
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package structs
import "time"
// CascadeMergeRule represents a cascade merge rule
type CascadeMergeRule struct {
ID int64 `json:"id"`
SourceBranch string `json:"source_branch"`
TargetBranch string `json:"target_branch"`
Enabled bool `json:"enabled"`
AutoMerge bool `json:"auto_merge"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// CreateCascadeMergeRuleOption options for creating a cascade merge rule
type CreateCascadeMergeRuleOption struct {
SourceBranch string `json:"source_branch" binding:"Required"`
TargetBranch string `json:"target_branch" binding:"Required"`
Enabled *bool `json:"enabled"`
AutoMerge *bool `json:"auto_merge"`
}
// EditCascadeMergeRuleOption options for editing a cascade merge rule
type EditCascadeMergeRuleOption struct {
SourceBranch *string `json:"source_branch"`
TargetBranch *string `json:"target_branch"`
Enabled *bool `json:"enabled"`
AutoMerge *bool `json:"auto_merge"`
}
+5
View File
@@ -3009,6 +3009,11 @@
"org.settings.issue_status_created": "Issue status created.",
"org.settings.issue_status_updated": "Issue status updated.",
"org.settings.issue_status_deleted": "Issue status deleted.",
"org.settings.issue_status_presets": "Status Presets",
"org.settings.issue_status_presets_desc": "Apply a preset template to replace your current statuses. Required statuses (Open/Closed) are preserved; others are deactivated and replaced.",
"org.settings.issue_status_preset_apply": "Apply Preset",
"org.settings.issue_status_preset_confirm": "This will deactivate your current custom statuses and replace them with the selected preset. Required statuses are preserved. Continue?",
"org.settings.issue_status_preset_applied": "Status preset applied successfully.",
"org.settings.issue_priorities": "Issue Priorities",
"org.settings.issue_priorities_desc": "Define priority levels for all repositories in this organization. Priorities appear in the issue sidebar.",
"org.settings.issue_priorities_empty": "No custom issue priorities defined yet.",
+22
View File
@@ -1262,6 +1262,23 @@ func Routes() *web.Router {
})
m.Post("/priority", bind(api.UpdateBranchProtectionPriories{}), mustNotBeArchived, repo.UpdateBranchProtectionPriories)
}, reqToken(), reqAdmin())
m.Group("/cascade_rules", func() {
m.Get("", repo.ListCascadeRules)
m.Post("", mustNotBeArchived, repo.CreateCascadeRule)
m.Group("/{id}", func() {
m.Get("", repo.GetCascadeRule)
m.Patch("", mustNotBeArchived, repo.EditCascadeRule)
m.Delete("", mustNotBeArchived, repo.DeleteCascadeRule)
})
}, reqToken(), reqAdmin())
m.Group("/security", func() {
m.Get("/alerts", repo.ListSecurityAlerts)
m.Get("/alerts/{id}", repo.GetSecurityAlert)
m.Patch("/alerts/{id}", reqToken(), reqAdmin(), repo.UpdateSecurityAlert)
m.Post("/scan", reqToken(), reqAdmin(), repo.TriggerSecurityScan)
m.Get("/config", repo.GetSecurityConfig)
m.Patch("/config", reqToken(), reqAdmin(), repo.UpdateSecurityConfig)
}, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true))
m.Group("/tags", func() {
m.Get("", repo.ListTags)
m.Get("/*", repo.GetTag)
@@ -1782,6 +1799,11 @@ func Routes() *web.Router {
m.Delete("/{id}", reqToken(), reqOrgOwnership(), org.DeleteOrgCustomField)
})
m.Get("/issue-statuses", org.ListIssueStatuses)
m.Group("/issue-statuses", func() {
m.Get("/presets", org.ListIssueStatusPresets)
m.Post("/presets/{preset}", reqToken(), reqOrgOwnership(), org.ApplyIssueStatusPreset)
m.Post("/copy/{source_org}", reqToken(), reqOrgOwnership(), org.CopyIssueStatusesFromOrg)
})
m.Get("/issue-priorities", org.ListIssuePriorities)
m.Get("/issue-types", org.ListIssueTypes)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly())
+105
View File
@@ -7,6 +7,7 @@ import (
"net/http"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
org_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/organization"
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
@@ -162,3 +163,107 @@ func ListIssueTypes(ctx *context.APIContext) {
}
ctx.JSON(http.StatusOK, result)
}
// ListIssueStatusPresets returns the available status preset templates.
func ListIssueStatusPresets(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/issue-statuses/presets organization orgListIssueStatusPresets
// ---
// summary: List available issue status presets
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// description: "StatusPresetList"
result := make([]*api.StatusPreset, 0, len(issues_model.StatusPresetNames()))
for _, name := range issues_model.StatusPresetNames() {
preset := issues_model.StatusPresets[name]
statuses := make([]*api.StatusPresetEntry, 0, len(preset.Statuses))
for _, s := range preset.Statuses {
statuses = append(statuses, &api.StatusPresetEntry{
Name: s.Name,
Color: s.Color,
Description: s.Description,
ClosesIssue: s.ClosesIssue,
IsRequired: s.IsRequired,
})
}
result = append(result, &api.StatusPreset{
Name: preset.Name,
Description: preset.Description,
Statuses: statuses,
})
}
ctx.JSON(http.StatusOK, result)
}
// ApplyIssueStatusPreset applies a status preset to an organization.
func ApplyIssueStatusPreset(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/issue-statuses/presets/{preset} organization orgApplyIssueStatusPreset
// ---
// summary: Apply a status preset to an organization
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: preset
// in: path
// description: preset name
// type: string
// required: true
// responses:
// "204":
// description: "StatusPresetApplied"
// "404":
// "$ref": "#/responses/notFound"
presetName := ctx.PathParam("preset")
if err := issues_model.ApplyStatusPreset(ctx, ctx.Org.Organization.ID, presetName); err != nil {
ctx.APIErrorNotFound()
return
}
ctx.Status(http.StatusNoContent)
}
// CopyIssueStatusesFromOrg copies status definitions from another organization.
func CopyIssueStatusesFromOrg(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/issue-statuses/copy/{source_org} organization orgCopyIssueStatuses
// ---
// summary: Copy issue statuses from another organization
// parameters:
// - name: org
// in: path
// description: target organization name
// type: string
// required: true
// - name: source_org
// in: path
// description: source organization name to copy from
// type: string
// required: true
// responses:
// "204":
// description: "StatusesCopied"
// "404":
// "$ref": "#/responses/notFound"
sourceOrgName := ctx.PathParam("source_org")
sourceOrg, err := org_model.GetOrgByName(ctx, sourceOrgName)
if err != nil {
ctx.APIErrorNotFound()
return
}
if err := issues_model.CopyStatusesFromOrg(ctx, sourceOrg.ID, ctx.Org.Organization.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
+151
View File
@@ -0,0 +1,151 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package repo
import (
"encoding/json"
"net/http"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
func toCascadeRuleAPI(rule *repo_model.CascadeMergeRule) *api.CascadeMergeRule {
return &api.CascadeMergeRule{
ID: rule.ID,
SourceBranch: rule.SourceBranch,
TargetBranch: rule.TargetBranch,
Enabled: rule.Enabled,
AutoMerge: rule.AutoMerge,
CreatedAt: rule.CreatedUnix.AsTime(),
UpdatedAt: rule.UpdatedUnix.AsTime(),
}
}
func ListCascadeRules(ctx *context.APIContext) {
rules, err := repo_model.GetCascadeRulesByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiRules := make([]*api.CascadeMergeRule, len(rules))
for i, rule := range rules {
apiRules[i] = toCascadeRuleAPI(rule)
}
ctx.JSON(http.StatusOK, apiRules)
}
func CreateCascadeRule(ctx *context.APIContext) {
var req api.CreateCascadeMergeRuleOption
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
ctx.APIError(http.StatusBadRequest, err)
return
}
if req.SourceBranch == "" || req.TargetBranch == "" {
ctx.APIError(http.StatusUnprocessableEntity, "source_branch and target_branch are required")
return
}
if req.SourceBranch == req.TargetBranch {
ctx.APIError(http.StatusUnprocessableEntity, "source_branch and target_branch must be different")
return
}
rule := &repo_model.CascadeMergeRule{
RepoID: ctx.Repo.Repository.ID,
SourceBranch: req.SourceBranch,
TargetBranch: req.TargetBranch,
Enabled: true,
}
if req.Enabled != nil {
rule.Enabled = *req.Enabled
}
if req.AutoMerge != nil {
rule.AutoMerge = *req.AutoMerge
}
if err := repo_model.CreateCascadeRule(ctx, rule); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, toCascadeRuleAPI(rule))
}
func GetCascadeRule(ctx *context.APIContext) {
rule, err := repo_model.GetCascadeRuleByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if rule == nil || rule.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorNotFound()
return
}
ctx.JSON(http.StatusOK, toCascadeRuleAPI(rule))
}
func EditCascadeRule(ctx *context.APIContext) {
rule, err := repo_model.GetCascadeRuleByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if rule == nil || rule.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorNotFound()
return
}
var req api.EditCascadeMergeRuleOption
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
ctx.APIError(http.StatusBadRequest, err)
return
}
if req.SourceBranch != nil {
rule.SourceBranch = *req.SourceBranch
}
if req.TargetBranch != nil {
rule.TargetBranch = *req.TargetBranch
}
if req.Enabled != nil {
rule.Enabled = *req.Enabled
}
if req.AutoMerge != nil {
rule.AutoMerge = *req.AutoMerge
}
if rule.SourceBranch == rule.TargetBranch {
ctx.APIError(http.StatusUnprocessableEntity, "source_branch and target_branch must be different")
return
}
if err := repo_model.UpdateCascadeRule(ctx, rule); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toCascadeRuleAPI(rule))
}
func DeleteCascadeRule(ctx *context.APIContext) {
rule, err := repo_model.GetCascadeRuleByID(ctx, ctx.PathParamInt64("id"))
if err != nil {
ctx.APIErrorInternal(err)
return
}
if rule == nil || rule.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorNotFound()
return
}
if err := repo_model.DeleteCascadeRule(ctx, ctx.Repo.Repository.ID, rule.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
+214
View File
@@ -0,0 +1,214 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package repo
import (
"encoding/json"
"net/http"
"time"
security_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/security"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
security_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/security"
)
type apiSecurityAlert struct {
ID int64 `json:"id"`
Scanner string `json:"scanner"`
Severity string `json:"severity"`
Status string `json:"status"`
RuleID string `json:"rule_id"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
FilePath string `json:"file_path,omitempty"`
LineNumber int `json:"line_number,omitempty"`
CommitSHA string `json:"commit_sha,omitempty"`
Fingerprint string `json:"fingerprint"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type apiSecurityConfig struct {
Enabled bool `json:"enabled"`
BlockOnPush bool `json:"block_on_push"`
SecretScanner bool `json:"secret_scanner"`
DependScanner bool `json:"depend_scanner"`
}
// ListSecurityAlerts returns all security alerts for a repo.
func ListSecurityAlerts(ctx *context.APIContext) {
status := ctx.FormString("status")
repoID := ctx.Repo.Repository.ID
var alerts []*security_model.SecurityAlert
var err error
switch status {
case "", "active":
alerts, err = security_model.GetActiveAlerts(ctx, repoID)
default:
alerts, err = security_model.GetAllAlerts(ctx, repoID)
}
if err != nil {
ctx.APIErrorInternal(err)
return
}
result := make([]*apiSecurityAlert, len(alerts))
for i, a := range alerts {
result[i] = toAPISecurityAlert(a)
}
ctx.JSON(http.StatusOK, result)
}
// GetSecurityAlert returns a single security alert.
func GetSecurityAlert(ctx *context.APIContext) {
id := ctx.PathParamInt64("id")
alert, err := security_model.GetAlertByID(ctx, id)
if err != nil {
ctx.APIErrorNotFound()
return
}
if alert.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorNotFound()
return
}
ctx.JSON(http.StatusOK, toAPISecurityAlert(alert))
}
// UpdateSecurityAlert changes the status of a security alert.
func UpdateSecurityAlert(ctx *context.APIContext) {
id := ctx.PathParamInt64("id")
alert, err := security_model.GetAlertByID(ctx, id)
if err != nil {
ctx.APIErrorNotFound()
return
}
if alert.RepoID != ctx.Repo.Repository.ID {
ctx.APIErrorNotFound()
return
}
var req struct {
Status string `json:"status"`
}
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
status := security_model.AlertStatus(req.Status)
if status != security_model.AlertStatusResolved && status != security_model.AlertStatusDismissed {
ctx.APIError(http.StatusUnprocessableEntity, "status must be 'resolved' or 'dismissed'")
return
}
if err := security_model.UpdateAlertStatus(ctx, id, status, ctx.Doer.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
alert, _ = security_model.GetAlertByID(ctx, id)
ctx.JSON(http.StatusOK, toAPISecurityAlert(alert))
}
// TriggerSecurityScan runs all enabled scanners against HEAD.
func TriggerSecurityScan(ctx *context.APIContext) {
commit := ctx.Repo.Commit
if commit == nil {
ctx.APIError(http.StatusBadRequest, "no commits in repository")
return
}
security_service.ScanOnPush(ctx, ctx.Repo.Repository, commit)
alerts, err := security_model.GetActiveAlerts(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
result := make([]*apiSecurityAlert, len(alerts))
for i, a := range alerts {
result[i] = toAPISecurityAlert(a)
}
ctx.JSON(http.StatusOK, result)
}
// GetSecurityConfig returns the scanner config for a repo.
func GetSecurityConfig(ctx *context.APIContext) {
cfg, err := security_model.GetScannerConfig(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPISecurityConfig(cfg))
}
// UpdateSecurityConfig updates the scanner config for a repo.
func UpdateSecurityConfig(ctx *context.APIContext) {
var req struct {
Enabled *bool `json:"enabled"`
BlockOnPush *bool `json:"block_on_push"`
SecretScanner *bool `json:"secret_scanner"`
DependScanner *bool `json:"depend_scanner"`
}
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
ctx.APIError(http.StatusUnprocessableEntity, err)
return
}
cfg, err := security_model.GetScannerConfig(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if req.Enabled != nil {
cfg.Enabled = *req.Enabled
}
if req.BlockOnPush != nil {
cfg.BlockOnPush = *req.BlockOnPush
}
if req.SecretScanner != nil {
cfg.SecretScanner = *req.SecretScanner
}
if req.DependScanner != nil {
cfg.DependScanner = *req.DependScanner
}
if err := security_model.SaveScannerConfig(ctx, cfg); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, toAPISecurityConfig(cfg))
}
func toAPISecurityAlert(a *security_model.SecurityAlert) *apiSecurityAlert {
return &apiSecurityAlert{
ID: a.ID,
Scanner: string(a.Scanner),
Severity: string(a.Severity),
Status: string(a.Status),
RuleID: a.RuleID,
Title: a.Title,
Description: a.Description,
FilePath: a.FilePath,
LineNumber: a.LineNumber,
CommitSHA: a.CommitSHA,
Fingerprint: a.Fingerprint,
CreatedAt: a.CreatedUnix.AsTime(),
UpdatedAt: a.UpdatedUnix.AsTime(),
}
}
func toAPISecurityConfig(cfg *security_model.SecurityScannerConfig) *apiSecurityConfig {
return &apiSecurityConfig{
Enabled: cfg.Enabled,
BlockOnPush: cfg.BlockOnPush,
SecretScanner: cfg.SecretScanner,
DependScanner: cfg.DependScanner,
}
}
+2
View File
@@ -38,6 +38,7 @@ import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/auth"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/auth/source/oauth2"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/automerge"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/cascade"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/cron"
feed_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/feed"
indexer_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/indexer"
@@ -153,6 +154,7 @@ func InitWebInstalled(ctx context.Context) {
mustInit(webhook.Init)
mustInit(pull_service.Init)
mustInit(automerge.Init)
cascade.Init()
mustInit(task.Init)
mustInit(repo_migrations.Init)
eventsource.GetManager().Init()
+18
View File
@@ -26,6 +26,7 @@ import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/agit"
gitea_context "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
pull_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/pull"
security_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/security"
)
type preReceiveContext struct {
@@ -151,6 +152,23 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
gitRepo := ctx.Repo.GitRepo
objectFormat := ctx.Repo.GetObjectFormat()
if newCommitID != objectFormat.EmptyObjectID().String() {
newCommit, err := gitRepo.GetCommit(newCommitID)
if err == nil {
if findings := security_service.ScanPushForSecrets(ctx, repo.ID, newCommit); len(findings) > 0 {
msg := fmt.Sprintf("Push rejected: %d secret(s) detected in commit %s", len(findings), newCommitID[:12])
for _, f := range findings {
msg += fmt.Sprintf("\n - %s in %s:%d", f.Title, f.FilePath, f.LineNumber)
}
log.Warn("Secret scan blocked push to %s in %-v: %d findings", branchName, repo, len(findings))
ctx.JSON(http.StatusForbidden, private.Response{
UserMsg: msg,
})
return
}
}
}
defaultBranch := repo.DefaultBranch
if ctx.opts.IsWiki && repo.DefaultWikiBranch != "" {
defaultBranch = repo.DefaultWikiBranch
+26
View File
@@ -27,9 +27,35 @@ func SettingsIssueStatuses(ctx *context.Context) {
}
ctx.Data["IssueStatuses"] = defs
// Load preset names for the preset selector
presetNames := issues_model.StatusPresetNames()
type presetInfo struct {
Name string
Description string
}
presets := make([]presetInfo, 0, len(presetNames))
for _, name := range presetNames {
p := issues_model.StatusPresets[name]
presets = append(presets, presetInfo{Name: p.Name, Description: p.Description})
}
ctx.Data["StatusPresets"] = presets
ctx.HTML(http.StatusOK, tplOrgIssueStatuses)
}
// SettingsIssueStatusesApplyPresetPost applies a status preset to the org.
func SettingsIssueStatusesApplyPresetPost(ctx *context.Context) {
presetName := ctx.FormString("preset")
if err := issues_model.ApplyStatusPreset(ctx, ctx.Org.Organization.ID, presetName); err != nil {
ctx.Flash.Error("Unknown preset: " + presetName)
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-statuses")
return
}
ctx.Flash.Success(ctx.Tr("org.settings.issue_status_preset_applied"))
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-statuses")
}
// SettingsIssueStatusesCreatePost creates a new org-level issue status.
func SettingsIssueStatusesCreatePost(ctx *context.Context) {
sortOrder, _ := strconv.Atoi(ctx.FormString("sort_order"))
+1
View File
@@ -1091,6 +1091,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Group("/issue-statuses", func() {
m.Get("", org.SettingsIssueStatuses)
m.Post("", org.SettingsIssueStatusesCreatePost)
m.Post("/apply-preset", org.SettingsIssueStatusesApplyPresetPost)
m.Post("/{id}/edit", org.SettingsIssueStatusesEditPost)
m.Post("/{id}/delete", org.SettingsIssueStatusesDeletePost)
})
+103
View File
@@ -0,0 +1,103 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package cascade
import (
"context"
"fmt"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
notify_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/notify"
pull_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/pull"
)
func Init() {
notify_service.RegisterNotifier(&cascadeNotifier{})
}
type cascadeNotifier struct {
notify_service.NullNotifier
}
func (n *cascadeNotifier) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
handleCascade(ctx, doer, pr)
}
func (n *cascadeNotifier) AutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
handleCascade(ctx, doer, pr)
}
func handleCascade(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
if err := pr.LoadBaseRepo(ctx); err != nil {
log.Error("cascade: LoadBaseRepo for PR #%d: %v", pr.Index, err)
return
}
rules, err := repo_model.GetCascadeRulesForBranch(ctx, pr.BaseRepo.ID, pr.BaseBranch)
if err != nil {
log.Error("cascade: GetCascadeRulesForBranch repo=%d branch=%s: %v", pr.BaseRepo.ID, pr.BaseBranch, err)
return
}
for _, rule := range rules {
if err := createCascadePR(ctx, doer, pr, rule); err != nil {
log.Error("cascade: failed to create PR %s→%s in repo %d: %v", rule.SourceBranch, rule.TargetBranch, rule.RepoID, err)
}
}
}
func createCascadePR(ctx context.Context, doer *user_model.User, mergedPR *issues_model.PullRequest, rule *repo_model.CascadeMergeRule) error {
repo := mergedPR.BaseRepo
existingPRs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(ctx, repo.ID, rule.TargetBranch)
if err != nil {
return fmt.Errorf("check existing PRs: %w", err)
}
for _, existing := range existingPRs {
if existing.HeadBranch == rule.SourceBranch && existing.HeadRepoID == repo.ID {
log.Info("cascade: PR already exists %s→%s in %s/%s (#%d), skipping",
rule.SourceBranch, rule.TargetBranch, repo.OwnerName, repo.Name, existing.Index)
return nil
}
}
title := fmt.Sprintf("cascade: merge %s into %s", rule.SourceBranch, rule.TargetBranch)
body := fmt.Sprintf("Auto-created by cascade merge rule after PR #%d was merged.\n\nSource: `%s` → Target: `%s`",
mergedPR.Index, rule.SourceBranch, rule.TargetBranch)
issue := &issues_model.Issue{
RepoID: repo.ID,
Repo: repo,
Title: title,
Content: body,
PosterID: doer.ID,
Poster: doer,
IsPull: true,
}
pullRequest := &issues_model.PullRequest{
HeadRepoID: repo.ID,
BaseRepoID: repo.ID,
HeadBranch: rule.SourceBranch,
BaseBranch: rule.TargetBranch,
HeadRepo: repo,
BaseRepo: repo,
Type: issues_model.PullRequestGitea,
}
if err := pull_service.NewPullRequest(ctx, &pull_service.NewPullRequestOptions{
Repo: repo,
Issue: issue,
PullRequest: pullRequest,
}); err != nil {
return fmt.Errorf("NewPullRequest: %w", err)
}
log.Info("cascade: created PR #%d (%s→%s) in %s/%s",
issue.Index, rule.SourceBranch, rule.TargetBranch, repo.OwnerName, repo.Name)
return nil
}
+21
View File
@@ -12,6 +12,27 @@ import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
)
// ScanPushForSecrets checks a commit for secrets and returns findings.
// Used by the pre-receive hook to block pushes containing secrets.
func ScanPushForSecrets(ctx context.Context, repoID int64, commit *git.Commit) []Finding {
cfg, err := security_model.GetScannerConfig(ctx, repoID)
if err != nil {
log.Error("ScanPushForSecrets: GetScannerConfig: %v", err)
return nil
}
if !cfg.Enabled || !cfg.BlockOnPush || !cfg.SecretScanner {
return nil
}
scanner := NewSecretScanner()
findings, err := scanner.ScanCommit(commit)
if err != nil {
log.Error("ScanPushForSecrets: ScanCommit: %v", err)
return nil
}
return findings
}
// ScanOnPush runs enabled scanners against a commit pushed to the default branch.
// Called from services/repository/push.go on default branch pushes.
func ScanOnPush(ctx context.Context, repo *repo_model.Repository, commit *git.Commit) {
@@ -62,6 +62,28 @@
<div class="divider"></div>
<h5>{{ctx.Locale.Tr "org.settings.issue_status_presets"}}</h5>
<p class="text grey">{{ctx.Locale.Tr "org.settings.issue_status_presets_desc"}}</p>
<form class="ui form" method="post" action="{{.OrgLink}}/settings/issue-statuses/apply-preset">
{{.CsrfTokenHtml}}
<div class="inline fields">
<div class="field">
<select name="preset" class="ui dropdown">
{{range .StatusPresets}}
<option value="{{.Name}}">{{.Description}}</option>
{{end}}
</select>
</div>
<div class="field">
<button class="ui primary button" type="submit" onclick="return confirm('{{ctx.Locale.Tr "org.settings.issue_status_preset_confirm"}}')">
{{svg "octicon-checklist" 14}} {{ctx.Locale.Tr "org.settings.issue_status_preset_apply"}}
</button>
</div>
</div>
</form>
<div class="divider"></div>
<h5>{{ctx.Locale.Tr "org.settings.issue_status_add"}}</h5>
<form class="ui form" method="post" action="{{.OrgLink}}/settings/issue-statuses">
{{.CsrfTokenHtml}}