Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cfdb9b4f0a | |||
| 0109a2db12 | |||
| ad4451f23c | |||
| fe6ca172f6 |
@@ -4,7 +4,7 @@
|
|||||||
<name>MokoGitea</name>
|
<name>MokoGitea</name>
|
||||||
<org>MokoConsulting</org>
|
<org>MokoConsulting</org>
|
||||||
<description>Moko fork of Gitea — adding project board REST API endpoints and custom enhancements</description>
|
<description>Moko fork of Gitea — adding project board REST API endpoints and custom enhancements</description>
|
||||||
<version>05.46.00</version>
|
<version>05.31.00</version>
|
||||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||||
</identity>
|
</identity>
|
||||||
<governance>
|
<governance>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Automation
|
# INGROUP: moko-platform.Automation
|
||||||
# VERSION: 05.46.00
|
# VERSION: 05.31.00
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
name: "Universal: Issue Branch"
|
name: "Universal: Issue Branch"
|
||||||
|
|||||||
+2
-24
@@ -1,9 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to MokoGitea are documented here. Versions follow the format
|
All notable changes to MokoGitea are documented here. Versions follow the format
|
||||||
`v{upstream}-moko.{major}.{minor}` (e.g. `v1.26.1-moko.06.02`).
|
`v{upstream}-moko.{major}.{minor}` (e.g. `v1.26.1-moko.06.03`).
|
||||||
|
|
||||||
## [v1.26.1-moko.06] - 2026-06-04
|
## [v1.26.1-moko.06.03] - 2026-06-04
|
||||||
|
|
||||||
* FEATURES
|
* FEATURES
|
||||||
* feat(licenses): full commercial license management system
|
* feat(licenses): full commercial license management system
|
||||||
@@ -165,25 +165,3 @@ All notable changes to MokoGitea are documented here. Versions follow the format
|
|||||||
* Reopened 9 closed issues lacking documented testing proof
|
* Reopened 9 closed issues lacking documented testing proof
|
||||||
* Created `pending: testing` label for features awaiting verification
|
* Created `pending: testing` label for features awaiting verification
|
||||||
* Established policy: issues must not be closed without documented testing proof
|
* Established policy: issues must not be closed without documented testing proof
|
||||||
|
|
||||||
## [1.26.1](https://github.com/go-gitea/gitea/releases/tag/v1.26.1) - 2026-04-21
|
|
||||||
|
|
||||||
* BUGFIXES
|
|
||||||
* Add event.schedule context for schedule actions task (#37320) (#37348)
|
|
||||||
* Fix an issue where changing an organization's visibility caused problems when users had forked its repositories. (#37324) (#37344)
|
|
||||||
* Use modern "git update-index --cacheinfo" syntax to support more file names (#37338) (#37343)
|
|
||||||
* Fix URL related escaping for oauth2 (#37334) (#37340)
|
|
||||||
* When the requested arch rpm is missing fall back to noarch (#37236) (#37339)
|
|
||||||
* Fix actions concurrency groups cross-branch leak (#37311) (#37331)
|
|
||||||
* Fix bug when accessing user badges (#37321) (#37329)
|
|
||||||
* Fix AppFullLink (#37325) (#37328)
|
|
||||||
* Fix container auth for public instance (#37290) (#37294)
|
|
||||||
* Enhance GetActionWorkflow to support fallback references (#37189) (#37283)
|
|
||||||
* Fix vite manifest update masking build errors (#37279) (#37310)
|
|
||||||
* Fix Mermaid diagrams failing when node labels contain line breaks (#37296) (#37299)
|
|
||||||
* Use TriggerEvent instead of Event in workflow runs API response for scheduled runs (#37288) #37360
|
|
||||||
* Add URL to Learn more about blocking a user. (#37355) #37367
|
|
||||||
* Fix button layout shift when collapsing file tree in editor (#37363) #37375
|
|
||||||
* Fix org team assignee/reviewer lookups for team member permissions (#37365) #37391
|
|
||||||
* Fix repo init README EOL (#37388) #37399
|
|
||||||
* Fix: dump with default zip type produces uncompressed zip (#37401)#37402
|
|
||||||
|
|||||||
@@ -49,8 +49,9 @@ type IssuesOptions struct { //nolint:revive // export stutter
|
|||||||
UpdatedAfterUnix int64
|
UpdatedAfterUnix int64
|
||||||
UpdatedBeforeUnix int64
|
UpdatedBeforeUnix int64
|
||||||
// prioritize issues from this repo
|
// prioritize issues from this repo
|
||||||
PriorityRepoID int64
|
PriorityRepoID int64
|
||||||
IsArchived optional.Option[bool]
|
IsArchived optional.Option[bool]
|
||||||
|
CustomFieldFilters map[int64]string // field_id → required value (AND semantics)
|
||||||
Owner *user_model.User // issues permission scope, it could be an organization or a user
|
Owner *user_model.User // issues permission scope, it could be an organization or a user
|
||||||
Team *organization.Team // issues permission scope
|
Team *organization.Team // issues permission scope
|
||||||
Doer *user_model.User // issues permission scope
|
Doer *user_model.User // issues permission scope
|
||||||
@@ -211,6 +212,20 @@ func applyProjectCondition(sess *xorm.Session, opts *IssuesOptions) {
|
|||||||
// do not need to apply any condition
|
// do not need to apply any condition
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func applyCustomFieldCondition(sess *xorm.Session, opts *IssuesOptions) {
|
||||||
|
if len(opts.CustomFieldFilters) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Each filtered field adds a subquery: the issue must have a matching
|
||||||
|
// custom_field_value row for every specified field (AND semantics).
|
||||||
|
for fieldID, value := range opts.CustomFieldFilters {
|
||||||
|
subQuery := builder.Select("entity_id").From("custom_field_value").Where(
|
||||||
|
builder.Eq{"field_id": fieldID, "value": value, "entity_type": "issue"},
|
||||||
|
)
|
||||||
|
sess.And(builder.In("issue.id", subQuery))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func applyRepoConditions(sess *xorm.Session, opts *IssuesOptions) {
|
func applyRepoConditions(sess *xorm.Session, opts *IssuesOptions) {
|
||||||
if len(opts.RepoIDs) == 1 {
|
if len(opts.RepoIDs) == 1 {
|
||||||
opts.RepoCond = builder.Eq{"issue.repo_id": opts.RepoIDs[0]}
|
opts.RepoCond = builder.Eq{"issue.repo_id": opts.RepoIDs[0]}
|
||||||
@@ -278,6 +293,7 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
applyLabelsCondition(sess, opts)
|
applyLabelsCondition(sess, opts)
|
||||||
|
applyCustomFieldCondition(sess, opts)
|
||||||
|
|
||||||
if opts.Owner != nil {
|
if opts.Owner != nil {
|
||||||
sess.And(repo_model.UserOwnedRepoCond(opts.Owner.ID))
|
sess.And(repo_model.UserOwnedRepoCond(opts.Owner.ID))
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
|
|||||||
Doer: nil,
|
Doer: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
opts.CustomFieldFilters = options.CustomFieldFilters
|
||||||
|
|
||||||
if len(options.MilestoneIDs) == 1 && options.MilestoneIDs[0] == 0 {
|
if len(options.MilestoneIDs) == 1 && options.MilestoneIDs[0] == 0 {
|
||||||
opts.MilestoneIDs = []int64{db.NoConditionID}
|
opts.MilestoneIDs = []int64{db.NoConditionID}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
|
|||||||
}
|
}
|
||||||
|
|
||||||
searchOpt.Paginator = opts.Paginator
|
searchOpt.Paginator = opts.Paginator
|
||||||
|
searchOpt.CustomFieldFilters = opts.CustomFieldFilters
|
||||||
|
|
||||||
switch opts.SortType {
|
switch opts.SortType {
|
||||||
case "", "latest":
|
case "", "latest":
|
||||||
|
|||||||
@@ -114,6 +114,8 @@ type SearchOptions struct {
|
|||||||
Paginator *db.ListOptions
|
Paginator *db.ListOptions
|
||||||
|
|
||||||
SortBy SortBy // sort by field
|
SortBy SortBy // sort by field
|
||||||
|
|
||||||
|
CustomFieldFilters map[int64]string // field_id → required value (AND semantics, DB-only)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy returns a copy of the options.
|
// Copy returns a copy of the options.
|
||||||
|
|||||||
@@ -289,6 +289,12 @@ func SearchIssues(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfFilters, cfErr := parseAPICustomFieldFilters(ctx); cfErr != nil {
|
||||||
|
return
|
||||||
|
} else if len(cfFilters) > 0 {
|
||||||
|
searchOpt.CustomFieldFilters = cfFilters
|
||||||
|
}
|
||||||
|
|
||||||
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
|
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
@@ -517,6 +523,12 @@ func ListIssues(ctx *context.APIContext) {
|
|||||||
searchOpt.MentionID = optional.Some(mentionedByID)
|
searchOpt.MentionID = optional.Some(mentionedByID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfFilters, cfErr := parseAPICustomFieldFilters(ctx); cfErr != nil {
|
||||||
|
return
|
||||||
|
} else if len(cfFilters) > 0 {
|
||||||
|
searchOpt.CustomFieldFilters = cfFilters
|
||||||
|
}
|
||||||
|
|
||||||
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
|
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
@@ -553,6 +565,25 @@ func getUserIDForFilter(ctx *context.APIContext, queryName string) int64 {
|
|||||||
return user.ID
|
return user.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseAPICustomFieldFilters extracts cf_{fieldID}=value query parameters.
|
||||||
|
// Returns an error (and writes a 400 response) if a field ID is non-numeric or non-positive.
|
||||||
|
func parseAPICustomFieldFilters(ctx *context.APIContext) (map[int64]string, error) {
|
||||||
|
filters := make(map[int64]string)
|
||||||
|
for key, values := range ctx.Req.URL.Query() {
|
||||||
|
after, ok := strings.CutPrefix(key, "cf_")
|
||||||
|
if !ok || len(values) == 0 || values[0] == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fieldID, err := strconv.ParseInt(after, 10, 64)
|
||||||
|
if err != nil || fieldID <= 0 {
|
||||||
|
ctx.APIError(http.StatusBadRequest, fmt.Sprintf("invalid custom field filter: cf_%s must use a positive numeric field ID", after))
|
||||||
|
return nil, fmt.Errorf("invalid cf_ param")
|
||||||
|
}
|
||||||
|
filters[fieldID] = values[0]
|
||||||
|
}
|
||||||
|
return filters, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetIssue get an issue of a repository
|
// GetIssue get an issue of a repository
|
||||||
func GetIssue(ctx *context.APIContext) {
|
func GetIssue(ctx *context.APIContext) {
|
||||||
// swagger:operation GET /repos/{owner}/{repo}/issues/{index} issue issueGetIssue
|
// swagger:operation GET /repos/{owner}/{repo}/issues/{index} issue issueGetIssue
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ package repo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -521,20 +524,52 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID int64, projectI
|
|||||||
|
|
||||||
prepareIssueFilterExclusiveOrderScopes(ctx, preparedLabelFilter.AllLabels)
|
prepareIssueFilterExclusiveOrderScopes(ctx, preparedLabelFilter.AllLabels)
|
||||||
|
|
||||||
|
// Parse custom field filters from query params (cf_{fieldID}={value}).
|
||||||
|
customFieldFilters := parseCustomFieldQueryParams(ctx.Req.URL.Query())
|
||||||
|
|
||||||
|
// Load custom field definitions for the filter UI.
|
||||||
|
// If this fails, clear filters so users don't get invisible filtering.
|
||||||
|
customFieldDefs, cfErr := issues_model.GetCustomFieldsByOwner(ctx, repo.OwnerID, issues_model.CustomFieldScopeIssue)
|
||||||
|
if cfErr != nil {
|
||||||
|
log.Error("prepareIssueFilterAndList: GetCustomFieldsByOwner: %v", cfErr)
|
||||||
|
customFieldFilters = make(map[int64]string)
|
||||||
|
}
|
||||||
|
ctx.Data["CustomFieldDefs"] = customFieldDefs
|
||||||
|
ctx.Data["CustomFieldFilters"] = customFieldFilters
|
||||||
|
// Build a query string fragment for cf_ params so they survive pagination/sort changes.
|
||||||
|
cfQuery := make(url.Values)
|
||||||
|
for fieldID, value := range customFieldFilters {
|
||||||
|
cfQuery.Set(fmt.Sprintf("cf_%d", fieldID), value)
|
||||||
|
}
|
||||||
|
ctx.Data["CustomFieldQueryString"] = cfQuery.Encode()
|
||||||
|
fieldOptions := make(map[int64][]string)
|
||||||
|
for _, f := range customFieldDefs {
|
||||||
|
if f.Options != "" {
|
||||||
|
var opts []string
|
||||||
|
if err := json.Unmarshal([]byte(f.Options), &opts); err != nil {
|
||||||
|
log.Error("prepareIssueFilterAndList: invalid options JSON for field %d (%s): %v", f.ID, f.Name, err)
|
||||||
|
} else {
|
||||||
|
fieldOptions[f.ID] = opts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.Data["CustomFieldOptions"] = fieldOptions
|
||||||
|
|
||||||
var keywordMatchedIssueIDs []int64
|
var keywordMatchedIssueIDs []int64
|
||||||
var issueStats *issues_model.IssueStats
|
var issueStats *issues_model.IssueStats
|
||||||
statsOpts := &issues_model.IssuesOptions{
|
statsOpts := &issues_model.IssuesOptions{
|
||||||
RepoIDs: []int64{repo.ID},
|
RepoIDs: []int64{repo.ID},
|
||||||
LabelIDs: preparedLabelFilter.SelectedLabelIDs,
|
LabelIDs: preparedLabelFilter.SelectedLabelIDs,
|
||||||
MilestoneIDs: mileIDs,
|
MilestoneIDs: mileIDs,
|
||||||
ProjectIDs: projectIDs,
|
ProjectIDs: projectIDs,
|
||||||
AssigneeID: assigneeID,
|
AssigneeID: assigneeID,
|
||||||
MentionedID: mentionedID,
|
MentionedID: mentionedID,
|
||||||
PosterID: posterUserID,
|
PosterID: posterUserID,
|
||||||
ReviewRequestedID: reviewRequestedID,
|
ReviewRequestedID: reviewRequestedID,
|
||||||
ReviewedID: reviewedID,
|
ReviewedID: reviewedID,
|
||||||
IsPull: isPullOption,
|
IsPull: isPullOption,
|
||||||
IssueIDs: nil,
|
IssueIDs: nil,
|
||||||
|
CustomFieldFilters: customFieldFilters,
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyword != "" {
|
if keyword != "" {
|
||||||
@@ -611,9 +646,10 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID int64, projectI
|
|||||||
ProjectIDs: projectIDs,
|
ProjectIDs: projectIDs,
|
||||||
IsClosed: isShowClosed,
|
IsClosed: isShowClosed,
|
||||||
IsPull: isPullOption,
|
IsPull: isPullOption,
|
||||||
LabelIDs: preparedLabelFilter.SelectedLabelIDs,
|
LabelIDs: preparedLabelFilter.SelectedLabelIDs,
|
||||||
SortType: sortType,
|
SortType: sortType,
|
||||||
IssueIDs: keywordMatchedIssueIDs,
|
IssueIDs: keywordMatchedIssueIDs,
|
||||||
|
CustomFieldFilters: customFieldFilters,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("DBIndexer.Search", err)
|
ctx.ServerError("DBIndexer.Search", err)
|
||||||
@@ -771,3 +807,17 @@ func Issues(ctx *context.Context) {
|
|||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplIssues)
|
ctx.HTML(http.StatusOK, tplIssues)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseCustomFieldQueryParams extracts cf_{fieldID}=value query parameters.
|
||||||
|
// Non-numeric or non-positive field IDs are silently skipped.
|
||||||
|
func parseCustomFieldQueryParams(query url.Values) map[int64]string {
|
||||||
|
filters := make(map[int64]string)
|
||||||
|
for key, values := range query {
|
||||||
|
if after, ok := strings.CutPrefix(key, "cf_"); ok && len(values) > 0 && values[0] != "" {
|
||||||
|
if fieldID, err := strconv.ParseInt(after, 10, 64); err == nil && fieldID > 0 {
|
||||||
|
filters[fieldID] = values[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filters
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{{$projectIDs := $.ProjectIDs}}
|
{{$projectIDs := $.ProjectIDs}}
|
||||||
{{$projectIDsQuery := SliceUtils.JoinInt64 $projectIDs}}
|
{{$projectIDsQuery := SliceUtils.JoinInt64 $projectIDs}}
|
||||||
{{$queryLink := QueryBuild "?" "q" $.Keyword "type" $.ViewType "sort" $.SortType "state" $.State "labels" $.SelectLabels "milestone" $.MilestoneID "project" $projectIDsQuery "assignee" $.AssigneeID "poster" $.PosterUsername "archived_labels" (Iif $.ShowArchivedLabels "true")}}
|
{{$queryLink := QueryBuild (print "?" $.CustomFieldQueryString) "q" $.Keyword "type" $.ViewType "sort" $.SortType "state" $.State "labels" $.SelectLabels "milestone" $.MilestoneID "project" $projectIDsQuery "assignee" $.AssigneeID "poster" $.PosterUsername "archived_labels" (Iif $.ShowArchivedLabels "true")}}
|
||||||
{{$showAllProjects := not $projectIDs}}
|
{{$showAllProjects := not $projectIDs}}
|
||||||
{{$showNoProjectSelected := and (eq (len $projectIDs) 1) (eq (index $projectIDs 0) -1)}}
|
{{$showNoProjectSelected := and (eq (len $projectIDs) 1) (eq (index $projectIDs 0) -1)}}
|
||||||
|
|
||||||
@@ -96,6 +96,32 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{if .CustomFieldDefs}}
|
||||||
|
<!-- Custom Field Filters -->
|
||||||
|
{{$cfFilters := .CustomFieldFilters}}
|
||||||
|
{{$cfOptions := .CustomFieldOptions}}
|
||||||
|
{{range $def := .CustomFieldDefs}}
|
||||||
|
{{$opts := index $cfOptions $def.ID}}
|
||||||
|
{{if $opts}}
|
||||||
|
{{$cfKey := printf "cf_%d" $def.ID}}
|
||||||
|
{{$currentVal := index $cfFilters $def.ID}}
|
||||||
|
<div class="item ui dropdown jump">
|
||||||
|
<span class="text {{if $currentVal}}tw-font-bold{{end}}">
|
||||||
|
{{$def.Name}}
|
||||||
|
</span>
|
||||||
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
|
<div class="menu">
|
||||||
|
<a class="{{if not $currentVal}}active {{end}}item" href="{{QueryBuild $queryLink $cfKey NIL}}">All</a>
|
||||||
|
<div class="divider"></div>
|
||||||
|
{{range $opt := $opts}}
|
||||||
|
<a class="{{if eq $opt $currentVal}}active {{end}}item" href="{{QueryBuild $queryLink $cfKey $opt}}">{{$opt}}</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<!-- Sort -->
|
<!-- Sort -->
|
||||||
<div class="item ui dropdown jump">
|
<div class="item ui dropdown jump">
|
||||||
<span class="text">
|
<span class="text">
|
||||||
|
|||||||
Reference in New Issue
Block a user