mirror of
1
Fork 0

Merge pull request '[gitea] week 2024-25 cherry pick (gitea/main -> forgejo)' (#4145) from earl-warren/wcp/2024-25 into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/4145
Reviewed-by: twenty-panda <twenty-panda@noreply.codeberg.org>
This commit is contained in:
Earl Warren 2024-06-18 07:56:20 +00:00
commit 328b5d79d3
69 changed files with 1069 additions and 282 deletions

View File

@ -87,7 +87,6 @@ code.gitea.io/gitea/models/repo
releaseSorter.Swap releaseSorter.Swap
SortReleases SortReleases
FindReposMapByIDs FindReposMapByIDs
SearchOrderBy.String
IsErrTopicNotExist IsErrTopicNotExist
ErrTopicNotExist.Error ErrTopicNotExist.Error
ErrTopicNotExist.Unwrap ErrTopicNotExist.Unwrap

View File

@ -22,6 +22,7 @@ linters:
- typecheck - typecheck
- unconvert - unconvert
- unused - unused
- unparam
- wastedassign - wastedassign
run: run:

View File

@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1715534503, "lastModified": 1717974879,
"narHash": "sha256-5ZSVkFadZbFP1THataCaSf0JH2cAH3S29hU9rrxTEqk=", "narHash": "sha256-GTO3C88+5DX171F/gVS3Qga/hOs/eRMxPFpiHq2t+D8=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "2057814051972fa1453ddfb0d98badbea9b83c06", "rev": "c7b821ba2e1e635ba5a76d299af62821cbcb09f3",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -30,6 +30,7 @@
# backend # backend
go_1_22 go_1_22
gofumpt
]; ];
}; };
} }

View File

@ -18,12 +18,6 @@ const (
SearchOrderByRecentUpdated SearchOrderBy = "updated_unix DESC" SearchOrderByRecentUpdated SearchOrderBy = "updated_unix DESC"
SearchOrderByOldest SearchOrderBy = "created_unix ASC" SearchOrderByOldest SearchOrderBy = "created_unix ASC"
SearchOrderByNewest SearchOrderBy = "created_unix DESC" SearchOrderByNewest SearchOrderBy = "created_unix DESC"
SearchOrderBySize SearchOrderBy = "size ASC"
SearchOrderBySizeReverse SearchOrderBy = "size DESC"
SearchOrderByGitSize SearchOrderBy = "git_size ASC"
SearchOrderByGitSizeReverse SearchOrderBy = "git_size DESC"
SearchOrderByLFSSize SearchOrderBy = "lfs_size ASC"
SearchOrderByLFSSizeReverse SearchOrderBy = "lfs_size DESC"
SearchOrderByID SearchOrderBy = "id ASC" SearchOrderByID SearchOrderBy = "id ASC"
SearchOrderByIDReverse SearchOrderBy = "id DESC" SearchOrderByIDReverse SearchOrderBy = "id DESC"
SearchOrderByStars SearchOrderBy = "num_stars ASC" SearchOrderByStars SearchOrderBy = "num_stars ASC"

View File

@ -215,16 +215,15 @@ func fileTimestampToTime(timestamp int64) time.Time {
return time.UnixMicro(timestamp) return time.UnixMicro(timestamp)
} }
func (f *file) loadMetaByPath() (*dbfsMeta, error) { func (f *file) loadMetaByPath() error {
var fileMeta dbfsMeta var fileMeta dbfsMeta
if ok, err := db.GetEngine(f.ctx).Where("full_path = ?", f.fullPath).Get(&fileMeta); err != nil { if ok, err := db.GetEngine(f.ctx).Where("full_path = ?", f.fullPath).Get(&fileMeta); err != nil {
return nil, err return err
} else if ok { } else if ok {
f.metaID = fileMeta.ID f.metaID = fileMeta.ID
f.blockSize = fileMeta.BlockSize f.blockSize = fileMeta.BlockSize
return &fileMeta, nil
} }
return nil, nil return nil
} }
func (f *file) open(flag int) (err error) { func (f *file) open(flag int) (err error) {
@ -288,10 +287,7 @@ func (f *file) createEmpty() error {
if err != nil { if err != nil {
return err return err
} }
if _, err = f.loadMetaByPath(); err != nil { return f.loadMetaByPath()
return err
}
return nil
} }
func (f *file) truncate() error { func (f *file) truncate() error {
@ -368,8 +364,5 @@ func buildPath(path string) string {
func newDbFile(ctx context.Context, path string) (*file, error) { func newDbFile(ctx context.Context, path string) (*file, error) {
path = buildPath(path) path = buildPath(path)
f := &file{ctx: ctx, fullPath: path, blockSize: defaultFileBlockSize} f := &file{ctx: ctx, fullPath: path, blockSize: defaultFileBlockSize}
if _, err := f.loadMetaByPath(); err != nil { return f, f.loadMetaByPath()
return nil, err
}
return f, nil
} }

View File

@ -110,6 +110,19 @@ func GetProtectedTagByID(ctx context.Context, id int64) (*ProtectedTag, error) {
return tag, nil return tag, nil
} }
// GetProtectedTagByNamePattern gets protected tag by name_pattern
func GetProtectedTagByNamePattern(ctx context.Context, repoID int64, pattern string) (*ProtectedTag, error) {
tag := &ProtectedTag{NamePattern: pattern, RepoID: repoID}
has, err := db.GetEngine(ctx).Get(tag)
if err != nil {
return nil, err
}
if !has {
return nil, nil
}
return tag, nil
}
// IsUserAllowedToControlTag checks if a user can control the specific tag. // IsUserAllowedToControlTag checks if a user can control the specific tag.
// It returns true if the tag name is not protected or the user is allowed to control it. // It returns true if the tag name is not protected or the user is allowed to control it.
func IsUserAllowedToControlTag(ctx context.Context, tags []*ProtectedTag, tagName string, userID int64) (bool, error) { func IsUserAllowedToControlTag(ctx context.Context, tags []*ProtectedTag, tagName string, userID int64) (bool, error) {

View File

@ -99,9 +99,9 @@ func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) {
} }
} }
func applyLimit(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { func applyLimit(sess *xorm.Session, opts *IssuesOptions) {
if opts.Paginator == nil || opts.Paginator.IsListAll() { if opts.Paginator == nil || opts.Paginator.IsListAll() {
return sess return
} }
start := 0 start := 0
@ -109,11 +109,9 @@ func applyLimit(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
start = (opts.Paginator.Page - 1) * opts.Paginator.PageSize start = (opts.Paginator.Page - 1) * opts.Paginator.PageSize
} }
sess.Limit(opts.Paginator.PageSize, start) sess.Limit(opts.Paginator.PageSize, start)
return sess
} }
func applyLabelsCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { func applyLabelsCondition(sess *xorm.Session, opts *IssuesOptions) {
if len(opts.LabelIDs) > 0 { if len(opts.LabelIDs) > 0 {
if opts.LabelIDs[0] == 0 { if opts.LabelIDs[0] == 0 {
sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_label)") sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_label)")
@ -136,11 +134,9 @@ func applyLabelsCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session
if len(opts.ExcludedLabelNames) > 0 { if len(opts.ExcludedLabelNames) > 0 {
sess.And(builder.NotIn("issue.id", BuildLabelNamesIssueIDsCondition(opts.ExcludedLabelNames))) sess.And(builder.NotIn("issue.id", BuildLabelNamesIssueIDsCondition(opts.ExcludedLabelNames)))
} }
return sess
} }
func applyMilestoneCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { func applyMilestoneCondition(sess *xorm.Session, opts *IssuesOptions) {
if len(opts.MilestoneIDs) == 1 && opts.MilestoneIDs[0] == db.NoConditionID { if len(opts.MilestoneIDs) == 1 && opts.MilestoneIDs[0] == db.NoConditionID {
sess.And("issue.milestone_id = 0") sess.And("issue.milestone_id = 0")
} else if len(opts.MilestoneIDs) > 0 { } else if len(opts.MilestoneIDs) > 0 {
@ -153,11 +149,9 @@ func applyMilestoneCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Sess
From("milestone"). From("milestone").
Where(builder.In("name", opts.IncludeMilestones))) Where(builder.In("name", opts.IncludeMilestones)))
} }
return sess
} }
func applyProjectCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { func applyProjectCondition(sess *xorm.Session, opts *IssuesOptions) {
if opts.ProjectID > 0 { // specific project if opts.ProjectID > 0 { // specific project
sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id"). sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id").
And("project_issue.project_id=?", opts.ProjectID) And("project_issue.project_id=?", opts.ProjectID)
@ -166,10 +160,9 @@ func applyProjectCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Sessio
} }
// opts.ProjectID == 0 means all projects, // opts.ProjectID == 0 means all projects,
// do not need to apply any condition // do not need to apply any condition
return sess
} }
func applyProjectColumnCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { func applyProjectColumnCondition(sess *xorm.Session, opts *IssuesOptions) {
// opts.ProjectColumnID == 0 means all project columns, // opts.ProjectColumnID == 0 means all project columns,
// do not need to apply any condition // do not need to apply any condition
if opts.ProjectColumnID > 0 { if opts.ProjectColumnID > 0 {
@ -177,10 +170,9 @@ func applyProjectColumnCondition(sess *xorm.Session, opts *IssuesOptions) *xorm.
} else if opts.ProjectColumnID == db.NoConditionID { } else if opts.ProjectColumnID == db.NoConditionID {
sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0})) sess.In("issue.id", builder.Select("issue_id").From("project_issue").Where(builder.Eq{"project_board_id": 0}))
} }
return sess
} }
func applyRepoConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { 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]}
} else if len(opts.RepoIDs) > 1 { } else if len(opts.RepoIDs) > 1 {
@ -195,10 +187,9 @@ func applyRepoConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session
if opts.RepoCond != nil { if opts.RepoCond != nil {
sess.And(opts.RepoCond) sess.And(opts.RepoCond)
} }
return sess
} }
func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session { func applyConditions(sess *xorm.Session, opts *IssuesOptions) {
if len(opts.IssueIDs) > 0 { if len(opts.IssueIDs) > 0 {
sess.In("issue.id", opts.IssueIDs) sess.In("issue.id", opts.IssueIDs)
} }
@ -261,8 +252,6 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session {
if opts.User != nil { if opts.User != nil {
sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.Value())) sess.And(issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.Value()))
} }
return sess
} }
// teamUnitsRepoCond returns query condition for those repo id in the special org team with special units access // teamUnitsRepoCond returns query condition for those repo id in the special org team with special units access
@ -339,22 +328,22 @@ func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organizati
return cond return cond
} }
func applyAssigneeCondition(sess *xorm.Session, assigneeID int64) *xorm.Session { func applyAssigneeCondition(sess *xorm.Session, assigneeID int64) {
return sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
And("issue_assignees.assignee_id = ?", assigneeID) And("issue_assignees.assignee_id = ?", assigneeID)
} }
func applyPosterCondition(sess *xorm.Session, posterID int64) *xorm.Session { func applyPosterCondition(sess *xorm.Session, posterID int64) {
return sess.And("issue.poster_id=?", posterID) sess.And("issue.poster_id=?", posterID)
} }
func applyMentionedCondition(sess *xorm.Session, mentionedID int64) *xorm.Session { func applyMentionedCondition(sess *xorm.Session, mentionedID int64) {
return sess.Join("INNER", "issue_user", "issue.id = issue_user.issue_id"). sess.Join("INNER", "issue_user", "issue.id = issue_user.issue_id").
And("issue_user.is_mentioned = ?", true). And("issue_user.is_mentioned = ?", true).
And("issue_user.uid = ?", mentionedID) And("issue_user.uid = ?", mentionedID)
} }
func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64) *xorm.Session { func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64) {
existInTeamQuery := builder.Select("team_user.team_id"). existInTeamQuery := builder.Select("team_user.team_id").
From("team_user"). From("team_user").
Where(builder.Eq{"team_user.uid": reviewRequestedID}) Where(builder.Eq{"team_user.uid": reviewRequestedID})
@ -375,11 +364,11 @@ func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64)
), ),
builder.In("review.id", maxReview), builder.In("review.id", maxReview),
)) ))
return sess.Where("issue.poster_id <> ?", reviewRequestedID). sess.Where("issue.poster_id <> ?", reviewRequestedID).
And(builder.In("issue.id", subQuery)) And(builder.In("issue.id", subQuery))
} }
func applyReviewedCondition(sess *xorm.Session, reviewedID int64) *xorm.Session { func applyReviewedCondition(sess *xorm.Session, reviewedID int64) {
// Query for pull requests where you are a reviewer or commenter, excluding // Query for pull requests where you are a reviewer or commenter, excluding
// any pull requests already returned by the review requested filter. // any pull requests already returned by the review requested filter.
notPoster := builder.Neq{"issue.poster_id": reviewedID} notPoster := builder.Neq{"issue.poster_id": reviewedID}
@ -406,11 +395,11 @@ func applyReviewedCondition(sess *xorm.Session, reviewedID int64) *xorm.Session
builder.In("type", CommentTypeComment, CommentTypeCode, CommentTypeReview), builder.In("type", CommentTypeComment, CommentTypeCode, CommentTypeReview),
)), )),
) )
return sess.And(notPoster, builder.Or(reviewed, commented)) sess.And(notPoster, builder.Or(reviewed, commented))
} }
func applySubscribedCondition(sess *xorm.Session, subscriberID int64) *xorm.Session { func applySubscribedCondition(sess *xorm.Session, subscriberID int64) {
return sess.And( sess.And(
builder. builder.
NotIn("issue.id", NotIn("issue.id",
builder.Select("issue_id"). builder.Select("issue_id").

View File

@ -28,7 +28,7 @@ type PullRequestsOptions struct {
MilestoneID int64 MilestoneID int64
} }
func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) (*xorm.Session, error) { func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) *xorm.Session {
sess := db.GetEngine(ctx).Where("pull_request.base_repo_id=?", baseRepoID) sess := db.GetEngine(ctx).Where("pull_request.base_repo_id=?", baseRepoID)
sess.Join("INNER", "issue", "pull_request.issue_id = issue.id") sess.Join("INNER", "issue", "pull_request.issue_id = issue.id")
@ -46,7 +46,7 @@ func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullR
sess.And("issue.milestone_id=?", opts.MilestoneID) sess.And("issue.milestone_id=?", opts.MilestoneID)
} }
return sess, nil return sess
} }
func GetUnmergedPullRequestsByHeadInfoMax(ctx context.Context, repoID, olderThan int64, branch string) ([]*PullRequest, error) { func GetUnmergedPullRequestsByHeadInfoMax(ctx context.Context, repoID, olderThan int64, branch string) ([]*PullRequest, error) {
@ -136,23 +136,15 @@ func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptio
opts.Page = 1 opts.Page = 1
} }
countSession, err := listPullRequestStatement(ctx, baseRepoID, opts) countSession := listPullRequestStatement(ctx, baseRepoID, opts)
if err != nil {
log.Error("listPullRequestStatement: %v", err)
return nil, 0, err
}
maxResults, err := countSession.Count(new(PullRequest)) maxResults, err := countSession.Count(new(PullRequest))
if err != nil { if err != nil {
log.Error("Count PRs: %v", err) log.Error("Count PRs: %v", err)
return nil, maxResults, err return nil, maxResults, err
} }
findSession, err := listPullRequestStatement(ctx, baseRepoID, opts) findSession := listPullRequestStatement(ctx, baseRepoID, opts)
applySorts(findSession, opts.SortType, 0) applySorts(findSession, opts.SortType, 0)
if err != nil {
log.Error("listPullRequestStatement: %v", err)
return nil, maxResults, err
}
findSession = db.SetSessionPagination(findSession, opts) findSession = db.SetSessionPagination(findSession, opts)
prs := make([]*PullRequest, 0, opts.PageSize) prs := make([]*PullRequest, 0, opts.PageSize)
return prs, maxResults, findSession.Find(&prs) return prs, maxResults, findSession.Find(&prs)
@ -206,8 +198,10 @@ func (prs PullRequestList) LoadIssues(ctx context.Context) (IssueList, error) {
return nil, nil return nil, nil
} }
// Load issues. // Load issues which are not loaded
issueIDs := prs.GetIssueIDs() issueIDs := container.FilterSlice(prs, func(pr *PullRequest) (int64, bool) {
return pr.IssueID, pr.Issue == nil && pr.IssueID > 0
})
issues := make(map[int64]*Issue, len(issueIDs)) issues := make(map[int64]*Issue, len(issueIDs))
if err := db.GetEngine(ctx). if err := db.GetEngine(ctx).
In("id", issueIDs). In("id", issueIDs).
@ -243,10 +237,7 @@ func (prs PullRequestList) LoadIssues(ctx context.Context) (IssueList, error) {
// GetIssueIDs returns all issue ids // GetIssueIDs returns all issue ids
func (prs PullRequestList) GetIssueIDs() []int64 { func (prs PullRequestList) GetIssueIDs() []int64 {
return container.FilterSlice(prs, func(pr *PullRequest) (int64, bool) { return container.FilterSlice(prs, func(pr *PullRequest) (int64, bool) {
if pr.Issue == nil { return pr.IssueID, pr.IssueID > 0
return pr.IssueID, pr.IssueID > 0
}
return 0, false
}) })
} }

View File

@ -205,31 +205,6 @@ type SearchRepoOptions struct {
OnlyShowRelevant bool OnlyShowRelevant bool
} }
// SearchOrderBy is used to sort the result
type SearchOrderBy string
func (s SearchOrderBy) String() string {
return string(s)
}
// Strings for sorting result
const (
SearchOrderByAlphabetically SearchOrderBy = "name ASC"
SearchOrderByAlphabeticallyReverse SearchOrderBy = "name DESC"
SearchOrderByLeastUpdated SearchOrderBy = "updated_unix ASC"
SearchOrderByRecentUpdated SearchOrderBy = "updated_unix DESC"
SearchOrderByOldest SearchOrderBy = "created_unix ASC"
SearchOrderByNewest SearchOrderBy = "created_unix DESC"
SearchOrderBySize SearchOrderBy = "size ASC"
SearchOrderBySizeReverse SearchOrderBy = "size DESC"
SearchOrderByID SearchOrderBy = "id ASC"
SearchOrderByIDReverse SearchOrderBy = "id DESC"
SearchOrderByStars SearchOrderBy = "num_stars ASC"
SearchOrderByStarsReverse SearchOrderBy = "num_stars DESC"
SearchOrderByForks SearchOrderBy = "num_forks ASC"
SearchOrderByForksReverse SearchOrderBy = "num_forks DESC"
)
// UserOwnedRepoCond returns user ownered repositories // UserOwnedRepoCond returns user ownered repositories
func UserOwnedRepoCond(userID int64) builder.Cond { func UserOwnedRepoCond(userID int64) builder.Cond {
return builder.Eq{ return builder.Eq{

View File

@ -5,20 +5,48 @@ package repo
import "code.gitea.io/gitea/models/db" import "code.gitea.io/gitea/models/db"
// SearchOrderByMap represents all possible search order // OrderByMap represents all possible search order
var SearchOrderByMap = map[string]map[string]db.SearchOrderBy{ var OrderByMap = map[string]map[string]db.SearchOrderBy{
"asc": { "asc": {
"alpha": "owner_name ASC, name ASC", "alpha": "owner_name ASC, name ASC",
"created": db.SearchOrderByOldest, "created": db.SearchOrderByOldest,
"updated": db.SearchOrderByLeastUpdated, "updated": db.SearchOrderByLeastUpdated,
"size": db.SearchOrderBySize, "size": "size ASC",
"id": db.SearchOrderByID, "git_size": "git_size ASC",
"lfs_size": "lfs_size ASC",
"id": db.SearchOrderByID,
"stars": db.SearchOrderByStars,
"forks": db.SearchOrderByForks,
}, },
"desc": { "desc": {
"alpha": "owner_name DESC, name DESC", "alpha": "owner_name DESC, name DESC",
"created": db.SearchOrderByNewest, "created": db.SearchOrderByNewest,
"updated": db.SearchOrderByRecentUpdated, "updated": db.SearchOrderByRecentUpdated,
"size": db.SearchOrderBySizeReverse, "size": "size DESC",
"id": db.SearchOrderByIDReverse, "git_size": "git_size DESC",
"lfs_size": "lfs_size DESC",
"id": db.SearchOrderByIDReverse,
"stars": db.SearchOrderByStarsReverse,
"forks": db.SearchOrderByForksReverse,
}, },
} }
// OrderByFlatMap is similar to OrderByMap but use human language keywords
// to decide between asc and desc
var OrderByFlatMap = map[string]db.SearchOrderBy{
"newest": OrderByMap["desc"]["created"],
"oldest": OrderByMap["asc"]["created"],
"leastupdate": OrderByMap["asc"]["updated"],
"reversealphabetically": OrderByMap["desc"]["alpha"],
"alphabetically": OrderByMap["asc"]["alpha"],
"reversesize": OrderByMap["desc"]["size"],
"size": OrderByMap["asc"]["size"],
"reversegitsize": OrderByMap["desc"]["git_size"],
"gitsize": OrderByMap["asc"]["git_size"],
"reverselfssize": OrderByMap["desc"]["lfs_size"],
"lfssize": OrderByMap["asc"]["lfs_size"],
"moststars": OrderByMap["desc"]["stars"],
"feweststars": OrderByMap["asc"]["stars"],
"mostforks": OrderByMap["desc"]["forks"],
"fewestforks": OrderByMap["asc"]["forks"],
}

View File

@ -18,7 +18,7 @@ func parseIntParam(value, param, algorithmName, config string, previousErr error
return parsed, previousErr // <- Keep the previous error as this function should still return an error once everything has been checked if any call failed return parsed, previousErr // <- Keep the previous error as this function should still return an error once everything has been checked if any call failed
} }
func parseUIntParam(value, param, algorithmName, config string, previousErr error) (uint64, error) { func parseUIntParam(value, param, algorithmName, config string, previousErr error) (uint64, error) { //nolint:unparam
parsed, err := strconv.ParseUint(value, 10, 64) parsed, err := strconv.ParseUint(value, 10, 64)
if err != nil { if err != nil {
log.Error("invalid integer for %s representation in %s hash spec %s", param, algorithmName, config) log.Error("invalid integer for %s representation in %s hash spec %s", param, algorithmName, config)

View File

@ -42,20 +42,19 @@ var (
) )
// loadGitVersion returns current Git version from shell. Internal usage only. // loadGitVersion returns current Git version from shell. Internal usage only.
func loadGitVersion() (*version.Version, error) { func loadGitVersion() error {
// doesn't need RWMutex because it's executed by Init() // doesn't need RWMutex because it's executed by Init()
if gitVersion != nil { if gitVersion != nil {
return gitVersion, nil return nil
} }
stdout, _, runErr := NewCommand(DefaultContext, "version").RunStdString(nil) stdout, _, runErr := NewCommand(DefaultContext, "version").RunStdString(nil)
if runErr != nil { if runErr != nil {
return nil, runErr return runErr
} }
fields := strings.Fields(stdout) fields := strings.Fields(stdout)
if len(fields) < 3 { if len(fields) < 3 {
return nil, fmt.Errorf("invalid git version output: %s", stdout) return fmt.Errorf("invalid git version output: %s", stdout)
} }
var versionString string var versionString string
@ -70,7 +69,7 @@ func loadGitVersion() (*version.Version, error) {
var err error var err error
gitVersion, err = version.NewVersion(versionString) gitVersion, err = version.NewVersion(versionString)
return gitVersion, err return err
} }
// SetExecutablePath changes the path of git executable and checks the file permission and version. // SetExecutablePath changes the path of git executable and checks the file permission and version.
@ -85,7 +84,7 @@ func SetExecutablePath(path string) error {
} }
GitExecutable = absPath GitExecutable = absPath
_, err = loadGitVersion() err = loadGitVersion()
if err != nil { if err != nil {
return fmt.Errorf("unable to load git version: %w", err) return fmt.Errorf("unable to load git version: %w", err)
} }
@ -312,7 +311,7 @@ func syncGitConfig() (err error) {
// CheckGitVersionAtLeast check git version is at least the constraint version // CheckGitVersionAtLeast check git version is at least the constraint version
func CheckGitVersionAtLeast(atLeast string) error { func CheckGitVersionAtLeast(atLeast string) error {
if _, err := loadGitVersion(); err != nil { if err := loadGitVersion(); err != nil {
return err return err
} }
atLeastVersion, err := version.NewVersion(atLeast) atLeastVersion, err := version.NewVersion(atLeast)
@ -327,7 +326,7 @@ func CheckGitVersionAtLeast(atLeast string) error {
// CheckGitVersionEqual checks if the git version is equal to the constraint version. // CheckGitVersionEqual checks if the git version is equal to the constraint version.
func CheckGitVersionEqual(equal string) error { func CheckGitVersionEqual(equal string) error {
if _, err := loadGitVersion(); err != nil { if err := loadGitVersion(); err != nil {
return err return err
} }
atLeastVersion, err := version.NewVersion(equal) atLeastVersion, err := version.NewVersion(equal)

View File

@ -34,13 +34,13 @@ type ObjectFormat interface {
ComputeHash(t ObjectType, content []byte) ObjectID ComputeHash(t ObjectType, content []byte) ObjectID
} }
func computeHash(dst []byte, hasher hash.Hash, t ObjectType, content []byte) []byte { func computeHash(dst []byte, hasher hash.Hash, t ObjectType, content []byte) {
_, _ = hasher.Write(t.Bytes()) _, _ = hasher.Write(t.Bytes())
_, _ = hasher.Write([]byte(" ")) _, _ = hasher.Write([]byte(" "))
_, _ = hasher.Write([]byte(strconv.Itoa(len(content)))) _, _ = hasher.Write([]byte(strconv.Itoa(len(content))))
_, _ = hasher.Write([]byte{0}) _, _ = hasher.Write([]byte{0})
_, _ = hasher.Write(content) _, _ = hasher.Write(content)
return hasher.Sum(dst) hasher.Sum(dst)
} }
/* SHA1 Type */ /* SHA1 Type */

View File

@ -38,6 +38,12 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
searchOpt.MilestoneIDs = opts.MilestoneIDs searchOpt.MilestoneIDs = opts.MilestoneIDs
} }
if opts.ProjectID > 0 {
searchOpt.ProjectID = optional.Some(opts.ProjectID)
} else if opts.ProjectID == -1 { // FIXME: this is inconsistent from other places
searchOpt.ProjectID = optional.Some[int64](0) // Those issues with no project(projectid==0)
}
// See the comment of issues_model.SearchOptions for the reason why we need to convert // See the comment of issues_model.SearchOptions for the reason why we need to convert
convertID := func(id int64) optional.Option[int64] { convertID := func(id int64) optional.Option[int64] {
if id > 0 { if id > 0 {
@ -49,7 +55,6 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
return nil return nil
} }
searchOpt.ProjectID = convertID(opts.ProjectID)
searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID) searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID)
searchOpt.PosterID = convertID(opts.PosterID) searchOpt.PosterID = convertID(opts.PosterID)
searchOpt.AssigneeID = convertID(opts.AssigneeID) searchOpt.AssigneeID = convertID(opts.AssigneeID)

View File

@ -211,7 +211,7 @@ func createRequest(ctx context.Context, method, url string, headers map[string]s
for key, value := range headers { for key, value := range headers {
req.Header.Set(key, value) req.Header.Set(key, value)
} }
req.Header.Set("Accept", MediaType) req.Header.Set("Accept", AcceptHeader)
return req, nil return req, nil
} }
@ -251,6 +251,6 @@ func handleErrorResponse(resp *http.Response) error {
return err return err
} }
log.Trace("ErrorResponse: %v", er) log.Trace("ErrorResponse(%v): %v", resp.Status, er)
return errors.New(er.Message) return errors.New(er.Message)
} }

View File

@ -155,7 +155,7 @@ func TestHTTPClientDownload(t *testing.T) {
hc := &http.Client{Transport: RoundTripFunc(func(req *http.Request) *http.Response { hc := &http.Client{Transport: RoundTripFunc(func(req *http.Request) *http.Response {
assert.Equal(t, "POST", req.Method) assert.Equal(t, "POST", req.Method)
assert.Equal(t, MediaType, req.Header.Get("Content-type")) assert.Equal(t, MediaType, req.Header.Get("Content-type"))
assert.Equal(t, MediaType, req.Header.Get("Accept")) assert.Equal(t, AcceptHeader, req.Header.Get("Accept"))
var batchRequest BatchRequest var batchRequest BatchRequest
err := json.NewDecoder(req.Body).Decode(&batchRequest) err := json.NewDecoder(req.Body).Decode(&batchRequest)
@ -263,7 +263,7 @@ func TestHTTPClientUpload(t *testing.T) {
hc := &http.Client{Transport: RoundTripFunc(func(req *http.Request) *http.Response { hc := &http.Client{Transport: RoundTripFunc(func(req *http.Request) *http.Response {
assert.Equal(t, "POST", req.Method) assert.Equal(t, "POST", req.Method)
assert.Equal(t, MediaType, req.Header.Get("Content-type")) assert.Equal(t, MediaType, req.Header.Get("Content-type"))
assert.Equal(t, MediaType, req.Header.Get("Accept")) assert.Equal(t, AcceptHeader, req.Header.Get("Accept"))
var batchRequest BatchRequest var batchRequest BatchRequest
err := json.NewDecoder(req.Body).Decode(&batchRequest) err := json.NewDecoder(req.Body).Decode(&batchRequest)

View File

@ -10,6 +10,8 @@ import (
const ( const (
// MediaType contains the media type for LFS server requests // MediaType contains the media type for LFS server requests
MediaType = "application/vnd.git-lfs+json" MediaType = "application/vnd.git-lfs+json"
// Some LFS servers offer content with other types, so fallback to '*/*' if application/vnd.git-lfs+json cannot be served
AcceptHeader = "application/vnd.git-lfs+json;q=0.9, */*;q=0.8"
) )
// BatchRequest contains multiple requests processed in one batch operation. // BatchRequest contains multiple requests processed in one batch operation.

View File

@ -37,6 +37,7 @@ func (a *BasicTransferAdapter) Download(ctx context.Context, l *Link) (io.ReadCl
if err != nil { if err != nil {
return nil, err return nil, err
} }
log.Debug("Download Request: %+v", req)
resp, err := performRequest(ctx, a.client, req) resp, err := performRequest(ctx, a.client, req)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -26,7 +26,7 @@ func TestBasicTransferAdapter(t *testing.T) {
p := Pointer{Oid: "b5a2c96250612366ea272ffac6d9744aaf4b45aacd96aa7cfcb931ee3b558259", Size: 5} p := Pointer{Oid: "b5a2c96250612366ea272ffac6d9744aaf4b45aacd96aa7cfcb931ee3b558259", Size: 5}
roundTripHandler := func(req *http.Request) *http.Response { roundTripHandler := func(req *http.Request) *http.Response {
assert.Equal(t, MediaType, req.Header.Get("Accept")) assert.Equal(t, AcceptHeader, req.Header.Get("Accept"))
assert.Equal(t, "test-value", req.Header.Get("test-header")) assert.Equal(t, "test-value", req.Header.Get("test-header"))
url := req.URL.String() url := req.URL.String()

View File

@ -48,7 +48,7 @@ var (
// hashCurrentPattern matches string that represents a commit SHA, e.g. d8a994ef243349f321568f9e36d5c3f444b99cae // hashCurrentPattern matches string that represents a commit SHA, e.g. d8a994ef243349f321568f9e36d5c3f444b99cae
// Although SHA1 hashes are 40 chars long, SHA256 are 64, the regex matches the hash from 7 to 64 chars in length // Although SHA1 hashes are 40 chars long, SHA256 are 64, the regex matches the hash from 7 to 64 chars in length
// so that abbreviated hash links can be used as well. This matches git and GitHub usability. // so that abbreviated hash links can be used as well. This matches git and GitHub usability.
hashCurrentPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-f]{7,64})(?:\s|$|\)|\]|[.,](\s|$))`) hashCurrentPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-f]{7,64})(?:\s|$|\)|\]|[.,:](\s|$))`)
// shortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax // shortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax
shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`) shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`)

View File

@ -381,6 +381,7 @@ func TestRegExp_sha1CurrentPattern(t *testing.T) {
"(abcdefabcdefabcdefabcdefabcdefabcdefabcd)", "(abcdefabcdefabcdefabcdefabcdefabcdefabcd)",
"[abcdefabcdefabcdefabcdefabcdefabcdefabcd]", "[abcdefabcdefabcdefabcdefabcdefabcdefabcd]",
"abcdefabcdefabcdefabcdefabcdefabcdefabcd.", "abcdefabcdefabcdefabcdefabcdefabcdefabcd.",
"abcdefabcdefabcdefabcdefabcdefabcdefabcd:",
} }
falseTestCases := []string{ falseTestCases := []string{
"test", "test",

View File

@ -9,9 +9,9 @@ import (
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/markup/common" "code.gitea.io/gitea/modules/markup/common"
"code.gitea.io/gitea/modules/util"
"github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/util"
) )
type prefixedIDs struct { type prefixedIDs struct {
@ -36,7 +36,7 @@ func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
if !bytes.HasPrefix(result, []byte("user-content-")) { if !bytes.HasPrefix(result, []byte("user-content-")) {
result = append([]byte("user-content-"), result...) result = append([]byte("user-content-"), result...)
} }
if p.values.Add(util.BytesToReadOnlyString(result)) { if p.values.Add(util.UnsafeBytesToString(result)) {
return result return result
} }
for i := 1; ; i++ { for i := 1; ; i++ {
@ -49,7 +49,7 @@ func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
// Put puts a given element id to the used ids table. // Put puts a given element id to the used ids table.
func (p *prefixedIDs) Put(value []byte) { func (p *prefixedIDs) Put(value []byte) {
p.values.Add(util.BytesToReadOnlyString(value)) p.values.Add(util.UnsafeBytesToString(value))
} }
func newPrefixedIDs() *prefixedIDs { func newPrefixedIDs() *prefixedIDs {

View File

@ -48,7 +48,7 @@ func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Nod
return ast.WalkContinue, nil return ast.WalkContinue, nil
} }
func (g *ASTTransformer) transformCodeSpan(ctx *markup.RenderContext, v *ast.CodeSpan, reader text.Reader) { func (g *ASTTransformer) transformCodeSpan(_ *markup.RenderContext, v *ast.CodeSpan, reader text.Reader) {
colorContent := v.Text(reader.Source()) colorContent := v.Text(reader.Source())
if matchColor(strings.ToLower(string(colorContent))) { if matchColor(strings.ToLower(string(colorContent))) {
v.AppendChild(v, NewColorPreview(colorContent)) v.AppendChild(v, NewColorPreview(colorContent))

View File

@ -7,10 +7,10 @@ import (
"fmt" "fmt"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/util"
"github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/text" "github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
) )
func (g *ASTTransformer) transformHeading(_ *markup.RenderContext, v *ast.Heading, reader text.Reader, tocList *[]markup.Header) { func (g *ASTTransformer) transformHeading(_ *markup.RenderContext, v *ast.Heading, reader text.Reader, tocList *[]markup.Header) {
@ -21,11 +21,11 @@ func (g *ASTTransformer) transformHeading(_ *markup.RenderContext, v *ast.Headin
} }
txt := v.Text(reader.Source()) txt := v.Text(reader.Source())
header := markup.Header{ header := markup.Header{
Text: util.BytesToReadOnlyString(txt), Text: util.UnsafeBytesToString(txt),
Level: v.Level, Level: v.Level,
} }
if id, found := v.AttributeString("id"); found { if id, found := v.AttributeString("id"); found {
header.ID = util.BytesToReadOnlyString(id.([]byte)) header.ID = util.UnsafeBytesToString(id.([]byte))
} }
*tocList = append(*tocList, header) *tocList = append(*tocList, header)
g.applyElementDir(v) g.applyElementDir(v)

View File

@ -6,6 +6,7 @@ package composer
import ( import (
"archive/zip" "archive/zip"
"io" "io"
"path"
"regexp" "regexp"
"strings" "strings"
@ -36,10 +37,14 @@ type Package struct {
Metadata *Metadata Metadata *Metadata
} }
// https://getcomposer.org/doc/04-schema.md
// Metadata represents the metadata of a Composer package // Metadata represents the metadata of a Composer package
type Metadata struct { type Metadata struct {
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Readme string `json:"readme,omitempty"`
Keywords []string `json:"keywords,omitempty"` Keywords []string `json:"keywords,omitempty"`
Comments Comments `json:"_comments,omitempty"`
Homepage string `json:"homepage,omitempty"` Homepage string `json:"homepage,omitempty"`
License Licenses `json:"license,omitempty"` License Licenses `json:"license,omitempty"`
Authors []Author `json:"authors,omitempty"` Authors []Author `json:"authors,omitempty"`
@ -74,6 +79,28 @@ func (l *Licenses) UnmarshalJSON(data []byte) error {
return nil return nil
} }
// Comments represents the comments of a Composer package
type Comments []string
// UnmarshalJSON reads from a string or array
func (c *Comments) UnmarshalJSON(data []byte) error {
switch data[0] {
case '"':
var value string
if err := json.Unmarshal(data, &value); err != nil {
return err
}
*c = Comments{value}
case '[':
values := make([]string, 0, 5)
if err := json.Unmarshal(data, &values); err != nil {
return err
}
*c = Comments(values)
}
return nil
}
// Author represents an author // Author represents an author
type Author struct { type Author struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
@ -101,14 +128,14 @@ func ParsePackage(r io.ReaderAt, size int64) (*Package, error) {
} }
defer f.Close() defer f.Close()
return ParseComposerFile(f) return ParseComposerFile(archive, path.Dir(file.Name), f)
} }
} }
return nil, ErrMissingComposerFile return nil, ErrMissingComposerFile
} }
// ParseComposerFile parses a composer.json file to retrieve the metadata of a Composer package // ParseComposerFile parses a composer.json file to retrieve the metadata of a Composer package
func ParseComposerFile(r io.Reader) (*Package, error) { func ParseComposerFile(archive *zip.Reader, pathPrefix string, r io.Reader) (*Package, error) {
var cj struct { var cj struct {
Name string `json:"name"` Name string `json:"name"`
Version string `json:"version"` Version string `json:"version"`
@ -137,6 +164,19 @@ func ParseComposerFile(r io.Reader) (*Package, error) {
cj.Type = "library" cj.Type = "library"
} }
if cj.Readme == "" {
cj.Readme = "README.md"
}
f, err := archive.Open(path.Join(pathPrefix, cj.Readme))
if err == nil {
// 10kb limit for readme content
buf, _ := io.ReadAll(io.LimitReader(f, 10*1024))
cj.Readme = string(buf)
_ = f.Close()
} else {
cj.Readme = ""
}
return &Package{ return &Package{
Name: cj.Name, Name: cj.Name,
Version: cj.Version, Version: cj.Version,

View File

@ -17,6 +17,8 @@ import (
const ( const (
name = "gitea/composer-package" name = "gitea/composer-package"
description = "Package Description" description = "Package Description"
readme = "Package Readme"
comments = "Package Comment"
packageType = "composer-plugin" packageType = "composer-plugin"
author = "Gitea Authors" author = "Gitea Authors"
email = "no.reply@gitea.io" email = "no.reply@gitea.io"
@ -41,7 +43,8 @@ const composerContent = `{
}, },
"require": { "require": {
"php": ">=7.2 || ^8.0" "php": ">=7.2 || ^8.0"
} },
"_comments": "` + comments + `"
}` }`
func TestLicenseUnmarshal(t *testing.T) { func TestLicenseUnmarshal(t *testing.T) {
@ -54,18 +57,30 @@ func TestLicenseUnmarshal(t *testing.T) {
assert.Equal(t, "MIT", l[0]) assert.Equal(t, "MIT", l[0])
} }
func TestCommentsUnmarshal(t *testing.T) {
var c Comments
assert.NoError(t, json.NewDecoder(strings.NewReader(`["comment"]`)).Decode(&c))
assert.Len(t, c, 1)
assert.Equal(t, "comment", c[0])
assert.NoError(t, json.NewDecoder(strings.NewReader(`"comment"`)).Decode(&c))
assert.Len(t, c, 1)
assert.Equal(t, "comment", c[0])
}
func TestParsePackage(t *testing.T) { func TestParsePackage(t *testing.T) {
createArchive := func(name, content string) []byte { createArchive := func(files map[string]string) []byte {
var buf bytes.Buffer var buf bytes.Buffer
archive := zip.NewWriter(&buf) archive := zip.NewWriter(&buf)
w, _ := archive.Create(name) for name, content := range files {
w.Write([]byte(content)) w, _ := archive.Create(name)
w.Write([]byte(content))
}
archive.Close() archive.Close()
return buf.Bytes() return buf.Bytes()
} }
t.Run("MissingComposerFile", func(t *testing.T) { t.Run("MissingComposerFile", func(t *testing.T) {
data := createArchive("dummy.txt", "") data := createArchive(map[string]string{"dummy.txt": ""})
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data))) cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
assert.Nil(t, cp) assert.Nil(t, cp)
@ -73,7 +88,7 @@ func TestParsePackage(t *testing.T) {
}) })
t.Run("MissingComposerFileInRoot", func(t *testing.T) { t.Run("MissingComposerFileInRoot", func(t *testing.T) {
data := createArchive("sub/sub/composer.json", "") data := createArchive(map[string]string{"sub/sub/composer.json": ""})
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data))) cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
assert.Nil(t, cp) assert.Nil(t, cp)
@ -81,43 +96,52 @@ func TestParsePackage(t *testing.T) {
}) })
t.Run("InvalidComposerFile", func(t *testing.T) { t.Run("InvalidComposerFile", func(t *testing.T) {
data := createArchive("composer.json", "") data := createArchive(map[string]string{"composer.json": ""})
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data))) cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
assert.Nil(t, cp) assert.Nil(t, cp)
assert.Error(t, err) assert.Error(t, err)
}) })
t.Run("Valid", func(t *testing.T) { t.Run("InvalidPackageName", func(t *testing.T) {
data := createArchive("composer.json", composerContent) data := createArchive(map[string]string{"composer.json": "{}"})
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data))) cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
assert.NoError(t, err)
assert.NotNil(t, cp)
})
}
func TestParseComposerFile(t *testing.T) {
t.Run("InvalidPackageName", func(t *testing.T) {
cp, err := ParseComposerFile(strings.NewReader(`{}`))
assert.Nil(t, cp) assert.Nil(t, cp)
assert.ErrorIs(t, err, ErrInvalidName) assert.ErrorIs(t, err, ErrInvalidName)
}) })
t.Run("InvalidPackageVersion", func(t *testing.T) { t.Run("InvalidPackageVersion", func(t *testing.T) {
cp, err := ParseComposerFile(strings.NewReader(`{"name": "gitea/composer-package", "version": "1.a.3"}`)) data := createArchive(map[string]string{"composer.json": `{"name": "gitea/composer-package", "version": "1.a.3"}`})
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
assert.Nil(t, cp) assert.Nil(t, cp)
assert.ErrorIs(t, err, ErrInvalidVersion) assert.ErrorIs(t, err, ErrInvalidVersion)
}) })
t.Run("InvalidReadmePath", func(t *testing.T) {
data := createArchive(map[string]string{"composer.json": `{"name": "gitea/composer-package", "readme": "sub/README.md"}`})
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
assert.NoError(t, err)
assert.NotNil(t, cp)
assert.Empty(t, cp.Metadata.Readme)
})
t.Run("Valid", func(t *testing.T) { t.Run("Valid", func(t *testing.T) {
cp, err := ParseComposerFile(strings.NewReader(composerContent)) data := createArchive(map[string]string{"composer.json": composerContent, "README.md": readme})
cp, err := ParsePackage(bytes.NewReader(data), int64(len(data)))
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, cp) assert.NotNil(t, cp)
assert.Equal(t, name, cp.Name) assert.Equal(t, name, cp.Name)
assert.Empty(t, cp.Version) assert.Empty(t, cp.Version)
assert.Equal(t, description, cp.Metadata.Description) assert.Equal(t, description, cp.Metadata.Description)
assert.Equal(t, readme, cp.Metadata.Readme)
assert.Len(t, cp.Metadata.Comments, 1)
assert.Equal(t, comments, cp.Metadata.Comments[0])
assert.Len(t, cp.Metadata.Authors, 1) assert.Len(t, cp.Metadata.Authors, 1)
assert.Equal(t, author, cp.Metadata.Authors[0].Name) assert.Equal(t, author, cp.Metadata.Authors[0].Name)
assert.Equal(t, email, cp.Metadata.Authors[0].Email) assert.Equal(t, email, cp.Metadata.Authors[0].Email)

View File

@ -185,8 +185,6 @@ func ParseDescription(r io.Reader) (*Package, error) {
} }
func setField(p *Package, data string) error { func setField(p *Package, data string) error {
const listDelimiter = ", "
if data == "" { if data == "" {
return nil return nil
} }
@ -215,19 +213,19 @@ func setField(p *Package, data string) error {
case "Description": case "Description":
p.Metadata.Description = value p.Metadata.Description = value
case "URL": case "URL":
p.Metadata.ProjectURL = splitAndTrim(value, listDelimiter) p.Metadata.ProjectURL = splitAndTrim(value)
case "License": case "License":
p.Metadata.License = value p.Metadata.License = value
case "Author": case "Author":
p.Metadata.Authors = splitAndTrim(authorReplacePattern.ReplaceAllString(value, ""), listDelimiter) p.Metadata.Authors = splitAndTrim(authorReplacePattern.ReplaceAllString(value, ""))
case "Depends": case "Depends":
p.Metadata.Depends = splitAndTrim(value, listDelimiter) p.Metadata.Depends = splitAndTrim(value)
case "Imports": case "Imports":
p.Metadata.Imports = splitAndTrim(value, listDelimiter) p.Metadata.Imports = splitAndTrim(value)
case "Suggests": case "Suggests":
p.Metadata.Suggests = splitAndTrim(value, listDelimiter) p.Metadata.Suggests = splitAndTrim(value)
case "LinkingTo": case "LinkingTo":
p.Metadata.LinkingTo = splitAndTrim(value, listDelimiter) p.Metadata.LinkingTo = splitAndTrim(value)
case "NeedsCompilation": case "NeedsCompilation":
p.Metadata.NeedsCompilation = value == "yes" p.Metadata.NeedsCompilation = value == "yes"
} }
@ -235,8 +233,8 @@ func setField(p *Package, data string) error {
return nil return nil
} }
func splitAndTrim(s, sep string) []string { func splitAndTrim(s string) []string {
items := strings.Split(s, sep) items := strings.Split(s, ", ")
for i := range items { for i := range items {
items[i] = strings.TrimSpace(items[i]) items[i] = strings.TrimSpace(items[i])
} }

View File

@ -14,8 +14,7 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup/mdstripper" "code.gitea.io/gitea/modules/markup/mdstripper"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"github.com/yuin/goldmark/util"
) )
var ( var (
@ -341,7 +340,7 @@ func FindRenderizableReferenceNumeric(content string, prOnly, crossLinkOnly bool
return false, nil return false, nil
} }
} }
r := getCrossReference(util.StringToReadOnlyBytes(content), match[2], match[3], false, prOnly) r := getCrossReference(util.UnsafeStringToBytes(content), match[2], match[3], false, prOnly)
if r == nil { if r == nil {
return false, nil return false, nil
} }

View File

@ -97,7 +97,7 @@ func decodeEnvSectionKey(encoded string) (ok bool, section, key string) {
// decodeEnvironmentKey decode the environment key to section and key // decodeEnvironmentKey decode the environment key to section and key
// The environment key is in the form of GITEA__SECTION__KEY or GITEA__SECTION__KEY__FILE // The environment key is in the form of GITEA__SECTION__KEY or GITEA__SECTION__KEY__FILE
func decodeEnvironmentKey(prefixRegexp *regexp.Regexp, suffixFile, envKey string) (ok bool, section, key string, useFileValue bool) { func decodeEnvironmentKey(prefixRegexp *regexp.Regexp, suffixFile, envKey string) (ok bool, section, key string, useFileValue bool) { //nolint:unparam
if strings.HasSuffix(envKey, suffixFile) { if strings.HasSuffix(envKey, suffixFile) {
useFileValue = true useFileValue = true
envKey = envKey[:len(envKey)-len(suffixFile)] envKey = envKey[:len(envKey)-len(suffixFile)]

View File

@ -122,7 +122,7 @@ const (
targetSecIsSec // target section is from the name seciont [name] targetSecIsSec // target section is from the name seciont [name]
) )
func getStorageSectionByType(rootCfg ConfigProvider, typ string) (ConfigSection, targetSecType, error) { func getStorageSectionByType(rootCfg ConfigProvider, typ string) (ConfigSection, targetSecType, error) { //nolint:unparam
targetSec, err := rootCfg.GetSection(storageSectionName + "." + typ) targetSec, err := rootCfg.GetSection(storageSectionName + "." + typ)
if err != nil { if err != nil {
if !IsValidStorageType(StorageType(typ)) { if !IsValidStorageType(StorageType(typ)) {

View File

@ -3,6 +3,8 @@
package structs package structs
import "time"
// Tag represents a repository tag // Tag represents a repository tag
type Tag struct { type Tag struct {
Name string `json:"name"` Name string `json:"name"`
@ -46,3 +48,29 @@ type TagArchiveDownloadCount struct {
Zip int64 `json:"zip"` Zip int64 `json:"zip"`
TarGz int64 `json:"tar_gz"` TarGz int64 `json:"tar_gz"`
} }
// TagProtection represents a tag protection
type TagProtection struct {
ID int64 `json:"id"`
NamePattern string `json:"name_pattern"`
WhitelistUsernames []string `json:"whitelist_usernames"`
WhitelistTeams []string `json:"whitelist_teams"`
// swagger:strfmt date-time
Created time.Time `json:"created_at"`
// swagger:strfmt date-time
Updated time.Time `json:"updated_at"`
}
// CreateTagProtectionOption options for creating a tag protection
type CreateTagProtectionOption struct {
NamePattern string `json:"name_pattern"`
WhitelistUsernames []string `json:"whitelist_usernames"`
WhitelistTeams []string `json:"whitelist_teams"`
}
// EditTagProtectionOption options for editing a tag protection
type EditTagProtectionOption struct {
NamePattern *string `json:"name_pattern"`
WhitelistUsernames []string `json:"whitelist_usernames"`
WhitelistTeams []string `json:"whitelist_teams"`
}

View File

@ -8,8 +8,7 @@ import (
"code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/util"
"github.com/yuin/goldmark/util"
) )
// DBStore can be used to store app state items in local filesystem // DBStore can be used to store app state items in local filesystem
@ -24,7 +23,7 @@ func (f *DBStore) Get(ctx context.Context, item StateItem) error {
if content == "" { if content == "" {
return nil return nil
} }
return json.Unmarshal(util.StringToReadOnlyBytes(content), item) return json.Unmarshal(util.UnsafeStringToBytes(content), item)
} }
// Set saves the state item // Set saves the state item
@ -33,5 +32,5 @@ func (f *DBStore) Set(ctx context.Context, item StateItem) error {
if err != nil { if err != nil {
return err return err
} }
return system.SaveAppStateContent(ctx, item.Name(), util.BytesToReadOnlyString(b)) return system.SaveAppStateContent(ctx, item.Name(), util.UnsafeBytesToString(b))
} }

View File

@ -15,10 +15,7 @@ import (
// GenerateKeyPair generates a public and private keypair // GenerateKeyPair generates a public and private keypair
func GenerateKeyPair(bits int) (string, string, error) { func GenerateKeyPair(bits int) (string, string, error) {
priv, _ := rsa.GenerateKey(rand.Reader, bits) priv, _ := rsa.GenerateKey(rand.Reader, bits)
privPem, err := pemBlockForPriv(priv) privPem := pemBlockForPriv(priv)
if err != nil {
return "", "", err
}
pubPem, err := pemBlockForPub(&priv.PublicKey) pubPem, err := pemBlockForPub(&priv.PublicKey)
if err != nil { if err != nil {
return "", "", err return "", "", err
@ -26,12 +23,12 @@ func GenerateKeyPair(bits int) (string, string, error) {
return privPem, pubPem, nil return privPem, pubPem, nil
} }
func pemBlockForPriv(priv *rsa.PrivateKey) (string, error) { func pemBlockForPriv(priv *rsa.PrivateKey) string {
privBytes := pem.EncodeToMemory(&pem.Block{ privBytes := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY", Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(priv), Bytes: x509.MarshalPKCS1PrivateKey(priv),
}) })
return string(privBytes), nil return string(privBytes)
} }
func pemBlockForPub(pub *rsa.PublicKey) (string, error) { func pemBlockForPub(pub *rsa.PublicKey) (string, error) {

View File

@ -6,8 +6,6 @@ package util
import ( import (
"bytes" "bytes"
"unicode" "unicode"
"github.com/yuin/goldmark/util"
) )
type sanitizedError struct { type sanitizedError struct {
@ -33,7 +31,7 @@ var schemeSep = []byte("://")
// SanitizeCredentialURLs remove all credentials in URLs (starting with "scheme://") for the input string: "https://user:pass@domain.com" => "https://sanitized-credential@domain.com" // SanitizeCredentialURLs remove all credentials in URLs (starting with "scheme://") for the input string: "https://user:pass@domain.com" => "https://sanitized-credential@domain.com"
func SanitizeCredentialURLs(s string) string { func SanitizeCredentialURLs(s string) string {
bs := util.StringToReadOnlyBytes(s) bs := UnsafeStringToBytes(s)
schemeSepPos := bytes.Index(bs, schemeSep) schemeSepPos := bytes.Index(bs, schemeSep)
if schemeSepPos == -1 || bytes.IndexByte(bs[schemeSepPos:], '@') == -1 { if schemeSepPos == -1 || bytes.IndexByte(bs[schemeSepPos:], '@') == -1 {
return s // fast return if there is no URL scheme or no userinfo return s // fast return if there is no URL scheme or no userinfo
@ -70,5 +68,5 @@ func SanitizeCredentialURLs(s string) string {
schemeSepPos = bytes.Index(bs, schemeSep) schemeSepPos = bytes.Index(bs, schemeSep)
} }
out = append(out, bs...) out = append(out, bs...)
return util.BytesToReadOnlyString(out) return UnsafeBytesToString(out)
} }

View File

@ -87,11 +87,11 @@ func ToSnakeCase(input string) string {
} }
// UnsafeBytesToString uses Go's unsafe package to convert a byte slice to a string. // UnsafeBytesToString uses Go's unsafe package to convert a byte slice to a string.
// TODO: replace all "goldmark/util.BytesToReadOnlyString" with this official approach
func UnsafeBytesToString(b []byte) string { func UnsafeBytesToString(b []byte) string {
return unsafe.String(unsafe.SliceData(b), len(b)) return unsafe.String(unsafe.SliceData(b), len(b))
} }
// UnsafeStringToBytes uses Go's unsafe package to convert a string to a byte slice.
func UnsafeStringToBytes(s string) []byte { func UnsafeStringToBytes(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s)) return unsafe.Slice(unsafe.StringData(s), len(s))
} }

View File

@ -35,6 +35,10 @@ func GetSiteCookie(req *http.Request, name string) string {
// SetSiteCookie returns given cookie value from request header. // SetSiteCookie returns given cookie value from request header.
func SetSiteCookie(resp http.ResponseWriter, name, value string, maxAge int) { func SetSiteCookie(resp http.ResponseWriter, name, value string, maxAge int) {
// Previous versions would use a cookie path with a trailing /.
// These are more specific than cookies without a trailing /, so
// we need to delete these if they exist.
deleteLegacySiteCookie(resp, name)
cookie := &http.Cookie{ cookie := &http.Cookie{
Name: name, Name: name,
Value: url.QueryEscape(value), Value: url.QueryEscape(value),
@ -46,10 +50,6 @@ func SetSiteCookie(resp http.ResponseWriter, name, value string, maxAge int) {
SameSite: setting.SessionConfig.SameSite, SameSite: setting.SessionConfig.SameSite,
} }
resp.Header().Add("Set-Cookie", cookie.String()) resp.Header().Add("Set-Cookie", cookie.String())
// Previous versions would use a cookie path with a trailing /.
// These are more specific than cookies without a trailing /, so
// we need to delete these if they exist.
deleteLegacySiteCookie(resp, name)
} }
// deleteLegacySiteCookie deletes the cookie with the given name at the cookie // deleteLegacySiteCookie deletes the cookie with the given name at the cookie

View File

@ -0,0 +1,3 @@
- [commit](https://codeberg.org/forgejo/forgejo/commit/b60e3ac7b4aeeb9b8760f43eea9576c0e23309e9) allow downloading draft releases assets.
- [commit](https://codeberg.org/forgejo/forgejo/commit/1fca15529ac8fefb60d86b0c1f4bec8dae9a8566) API endpoints for managing tag protection
- [commit](https://codeberg.org/forgejo/forgejo/commit/4334c705b5f9388b16af23c7e75a69d027d07d5e) extract and display readme and comments for Composer packages

View File

@ -0,0 +1,2 @@
- [commit](https://codeberg.org/forgejo/forgejo/commit/364922c6e4f28264add9e2501a352c25ad6a0993) when a repository is adopted, its object format is not set in the database
- [commit](https://codeberg.org/forgejo/forgejo/commit/e7f332a55d6a48a3f3b4f2bfa43d18455ac00acc) during a migration from bitbucket, LFS downloads fail

View File

@ -0,0 +1 @@
- [commit](https://codeberg.org/forgejo/forgejo/commit/358cd67c4f316f2d4f1d3be6dcb891dc04a2ff07) reduce memory usage for chunked artifact uploads to S3

View File

@ -241,16 +241,12 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) {
} }
// get upload file size // get upload file size
fileRealTotalSize, contentLength, err := getUploadFileSize(ctx) fileRealTotalSize, contentLength := getUploadFileSize(ctx)
if err != nil {
log.Error("Error get upload file size: %v", err)
ctx.Error(http.StatusInternalServerError, "Error get upload file size")
return
}
// get artifact retention days // get artifact retention days
expiredDays := setting.Actions.ArtifactRetentionDays expiredDays := setting.Actions.ArtifactRetentionDays
if queryRetentionDays := ctx.Req.URL.Query().Get("retentionDays"); queryRetentionDays != "" { if queryRetentionDays := ctx.Req.URL.Query().Get("retentionDays"); queryRetentionDays != "" {
var err error
expiredDays, err = strconv.ParseInt(queryRetentionDays, 10, 64) expiredDays, err = strconv.ParseInt(queryRetentionDays, 10, 64)
if err != nil { if err != nil {
log.Error("Error parse retention days: %v", err) log.Error("Error parse retention days: %v", err)

View File

@ -39,7 +39,7 @@ func saveUploadChunkBase(st storage.ObjectStorage, ctx *ArtifactContext,
r = io.TeeReader(r, hasher) r = io.TeeReader(r, hasher)
} }
// save chunk to storage // save chunk to storage
writtenSize, err := st.Save(storagePath, r, -1) writtenSize, err := st.Save(storagePath, r, contentSize)
if err != nil { if err != nil {
return -1, fmt.Errorf("save chunk to storage error: %v", err) return -1, fmt.Errorf("save chunk to storage error: %v", err)
} }
@ -208,7 +208,7 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st
// save merged file // save merged file
storagePath := fmt.Sprintf("%d/%d/%d.%s", artifact.RunID%255, artifact.ID%255, time.Now().UnixNano(), extension) storagePath := fmt.Sprintf("%d/%d/%d.%s", artifact.RunID%255, artifact.ID%255, time.Now().UnixNano(), extension)
written, err := st.Save(storagePath, mergedReader, -1) written, err := st.Save(storagePath, mergedReader, artifact.FileCompressedSize)
if err != nil { if err != nil {
return fmt.Errorf("save merged file error: %v", err) return fmt.Errorf("save merged file error: %v", err)
} }

View File

@ -43,7 +43,7 @@ func validateRunID(ctx *ArtifactContext) (*actions.ActionTask, int64, bool) {
return task, runID, true return task, runID, true
} }
func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) { func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) { //nolint:unparam
task := ctx.ActionTask task := ctx.ActionTask
runID, err := strconv.ParseInt(rawRunID, 10, 64) runID, err := strconv.ParseInt(rawRunID, 10, 64)
if err != nil || task.Job.RunID != runID { if err != nil || task.Job.RunID != runID {
@ -84,11 +84,11 @@ func parseArtifactItemPath(ctx *ArtifactContext) (string, string, bool) {
// getUploadFileSize returns the size of the file to be uploaded. // getUploadFileSize returns the size of the file to be uploaded.
// The raw size is the size of the file as reported by the header X-TFS-FileLength. // The raw size is the size of the file as reported by the header X-TFS-FileLength.
func getUploadFileSize(ctx *ArtifactContext) (int64, int64, error) { func getUploadFileSize(ctx *ArtifactContext) (int64, int64) {
contentLength := ctx.Req.ContentLength contentLength := ctx.Req.ContentLength
xTfsLength, _ := strconv.ParseInt(ctx.Req.Header.Get(artifactXTfsFileLengthHeader), 10, 64) xTfsLength, _ := strconv.ParseInt(ctx.Req.Header.Get(artifactXTfsFileLengthHeader), 10, 64)
if xTfsLength > 0 { if xTfsLength > 0 {
return xTfsLength, contentLength, nil return xTfsLength, contentLength
} }
return contentLength, contentLength, nil return contentLength, contentLength
} }

View File

@ -26,7 +26,7 @@ var uploadVersionMutex sync.Mutex
// saveAsPackageBlob creates a package blob from an upload // saveAsPackageBlob creates a package blob from an upload
// The uploaded blob gets stored in a special upload version to link them to the package/image // The uploaded blob gets stored in a special upload version to link them to the package/image
func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo) (*packages_model.PackageBlob, error) { func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo) (*packages_model.PackageBlob, error) { //nolint:unparam
pb := packages_service.NewPackageBlob(hsr) pb := packages_service.NewPackageBlob(hsr)
exists := false exists := false

View File

@ -36,7 +36,7 @@ func apiError(ctx *context.Context, status int, obj any) {
}) })
} }
func xmlResponse(ctx *context.Context, status int, obj any) { func xmlResponse(ctx *context.Context, status int, obj any) { //nolint:unparam
ctx.Resp.Header().Set("Content-Type", "application/atom+xml; charset=utf-8") ctx.Resp.Header().Set("Content-Type", "application/atom+xml; charset=utf-8")
ctx.Resp.WriteHeader(status) ctx.Resp.WriteHeader(status)
if _, err := ctx.Resp.Write([]byte(xml.Header)); err != nil { if _, err := ctx.Resp.Write([]byte(xml.Header)); err != nil {

View File

@ -1112,6 +1112,15 @@ func Routes() *web.Route {
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateTagOption{}), repo.CreateTag) m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateTagOption{}), repo.CreateTag)
m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteTag) m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteTag)
}, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true)) }, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true))
m.Group("/tag_protections", func() {
m.Combo("").Get(repo.ListTagProtection).
Post(bind(api.CreateTagProtectionOption{}), mustNotBeArchived, repo.CreateTagProtection)
m.Group("/{id}", func() {
m.Combo("").Get(repo.GetTagProtection).
Patch(bind(api.EditTagProtectionOption{}), mustNotBeArchived, repo.EditTagProtection).
Delete(repo.DeleteTagProtection)
})
}, reqToken(), reqAdmin())
m.Group("/actions", func() { m.Group("/actions", func() {
m.Get("/tasks", repo.ListActionTasks) m.Get("/tasks", repo.ListActionTasks)
}, reqRepoReader(unit.TypeActions), context.ReferencesGitRepo(true)) }, reqRepoReader(unit.TypeActions), context.ReferencesGitRepo(true))

View File

@ -64,7 +64,7 @@ func CompareDiff(ctx *context.APIContext) {
} }
} }
_, _, headGitRepo, ci, _, _ := parseCompareInfo(ctx, api.CreatePullRequestOption{ _, headGitRepo, ci, _, _ := parseCompareInfo(ctx, api.CreatePullRequestOption{
Base: infos[0], Base: infos[0],
Head: infos[1], Head: infos[1],
}) })

View File

@ -406,7 +406,7 @@ func CreatePullRequest(ctx *context.APIContext) {
) )
// Get repo/branch information // Get repo/branch information
_, headRepo, headGitRepo, compareInfo, baseBranch, headBranch := parseCompareInfo(ctx, form) headRepo, headGitRepo, compareInfo, baseBranch, headBranch := parseCompareInfo(ctx, form)
if ctx.Written() { if ctx.Written() {
return return
} }
@ -1051,7 +1051,7 @@ func MergePullRequest(ctx *context.APIContext) {
ctx.Status(http.StatusOK) ctx.Status(http.StatusOK)
} }
func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) (*user_model.User, *repo_model.Repository, *git.Repository, *git.CompareInfo, string, string) { func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) (*repo_model.Repository, *git.Repository, *git.CompareInfo, string, string) {
baseRepo := ctx.Repo.Repository baseRepo := ctx.Repo.Repository
// Get compared branches information // Get compared branches information
@ -1084,14 +1084,14 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
} else { } else {
ctx.Error(http.StatusInternalServerError, "GetUserByName", err) ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
} }
return nil, nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
headBranch = headInfos[1] headBranch = headInfos[1]
// The head repository can also point to the same repo // The head repository can also point to the same repo
isSameRepo = ctx.Repo.Owner.ID == headUser.ID isSameRepo = ctx.Repo.Owner.ID == headUser.ID
} else { } else {
ctx.NotFound() ctx.NotFound()
return nil, nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
ctx.Repo.PullRequest.SameRepo = isSameRepo ctx.Repo.PullRequest.SameRepo = isSameRepo
@ -1099,7 +1099,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
// Check if base branch is valid. // Check if base branch is valid.
if !ctx.Repo.GitRepo.IsBranchExist(baseBranch) && !ctx.Repo.GitRepo.IsTagExist(baseBranch) { if !ctx.Repo.GitRepo.IsBranchExist(baseBranch) && !ctx.Repo.GitRepo.IsTagExist(baseBranch) {
ctx.NotFound("BaseNotExist") ctx.NotFound("BaseNotExist")
return nil, nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
// Check if current user has fork of repository or in the same repository. // Check if current user has fork of repository or in the same repository.
@ -1107,7 +1107,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
if headRepo == nil && !isSameRepo { if headRepo == nil && !isSameRepo {
log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID) log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID)
ctx.NotFound("GetForkedRepo") ctx.NotFound("GetForkedRepo")
return nil, nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
var headGitRepo *git.Repository var headGitRepo *git.Repository
@ -1118,7 +1118,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
headGitRepo, err = gitrepo.OpenRepository(ctx, headRepo) headGitRepo, err = gitrepo.OpenRepository(ctx, headRepo)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "OpenRepository", err) ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
return nil, nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
} }
@ -1127,7 +1127,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
if err != nil { if err != nil {
headGitRepo.Close() headGitRepo.Close()
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
return nil, nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) { if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) {
if log.IsTrace() { if log.IsTrace() {
@ -1138,7 +1138,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
} }
headGitRepo.Close() headGitRepo.Close()
ctx.NotFound("Can't read pulls or can't read UnitTypeCode") ctx.NotFound("Can't read pulls or can't read UnitTypeCode")
return nil, nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
// user should have permission to read headrepo's codes // user should have permission to read headrepo's codes
@ -1146,7 +1146,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
if err != nil { if err != nil {
headGitRepo.Close() headGitRepo.Close()
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
return nil, nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
if !permHead.CanRead(unit.TypeCode) { if !permHead.CanRead(unit.TypeCode) {
if log.IsTrace() { if log.IsTrace() {
@ -1157,24 +1157,24 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption)
} }
headGitRepo.Close() headGitRepo.Close()
ctx.NotFound("Can't read headRepo UnitTypeCode") ctx.NotFound("Can't read headRepo UnitTypeCode")
return nil, nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
// Check if head branch is valid. // Check if head branch is valid.
if !headGitRepo.IsBranchExist(headBranch) && !headGitRepo.IsTagExist(headBranch) { if !headGitRepo.IsBranchExist(headBranch) && !headGitRepo.IsTagExist(headBranch) {
headGitRepo.Close() headGitRepo.Close()
ctx.NotFound() ctx.NotFound()
return nil, nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseBranch, headBranch, false, false) compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseBranch, headBranch, false, false)
if err != nil { if err != nil {
headGitRepo.Close() headGitRepo.Close()
ctx.Error(http.StatusInternalServerError, "GetCompareInfo", err) ctx.Error(http.StatusInternalServerError, "GetCompareInfo", err)
return nil, nil, nil, nil, "", "" return nil, nil, nil, "", ""
} }
return headUser, headRepo, headGitRepo, compareInfo, baseBranch, headBranch return headRepo, headGitRepo, compareInfo, baseBranch, headBranch
} }
// UpdatePullRequest merge PR's baseBranch into headBranch // UpdatePullRequest merge PR's baseBranch into headBranch

View File

@ -107,7 +107,7 @@ func Search(ctx *context.APIContext) {
// - name: sort // - name: sort
// in: query // in: query
// description: sort repos by attribute. Supported values are // description: sort repos by attribute. Supported values are
// "alpha", "created", "updated", "size", and "id". // "alpha", "created", "updated", "size", "git_size", "lfs_size", "stars", "forks" and "id".
// Default is "alpha" // Default is "alpha"
// type: string // type: string
// - name: order // - name: order
@ -184,7 +184,7 @@ func Search(ctx *context.APIContext) {
if len(sortOrder) == 0 { if len(sortOrder) == 0 {
sortOrder = "asc" sortOrder = "asc"
} }
if searchModeMap, ok := repo_model.SearchOrderByMap[sortOrder]; ok { if searchModeMap, ok := repo_model.OrderByMap[sortOrder]; ok {
if orderBy, ok := searchModeMap[sortMode]; ok { if orderBy, ok := searchModeMap[sortMode]; ok {
opts.OrderBy = orderBy opts.OrderBy = orderBy
} else { } else {

View File

@ -7,9 +7,13 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
git_model "code.gitea.io/gitea/models/git"
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
@ -314,3 +318,349 @@ func DeleteTag(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent) ctx.Status(http.StatusNoContent)
} }
// ListTagProtection lists tag protections for a repo
func ListTagProtection(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/tag_protections repository repoListTagProtection
// ---
// summary: List tag protections for a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/TagProtectionList"
repo := ctx.Repo.Repository
pts, err := git_model.GetProtectedTags(ctx, repo.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedTags", err)
return
}
apiPts := make([]*api.TagProtection, len(pts))
for i := range pts {
apiPts[i] = convert.ToTagProtection(ctx, pts[i], repo)
}
ctx.JSON(http.StatusOK, apiPts)
}
// GetTagProtection gets a tag protection
func GetTagProtection(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/tag_protections/{id} repository repoGetTagProtection
// ---
// summary: Get a specific tag protection for the repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of the tag protect to get
// type: integer
// required: true
// responses:
// "200":
// "$ref": "#/responses/TagProtection"
// "404":
// "$ref": "#/responses/notFound"
repo := ctx.Repo.Repository
id := ctx.ParamsInt64(":id")
pt, err := git_model.GetProtectedTagByID(ctx, id)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err)
return
}
if pt == nil || repo.ID != pt.RepoID {
ctx.NotFound()
return
}
ctx.JSON(http.StatusOK, convert.ToTagProtection(ctx, pt, repo))
}
// CreateTagProtection creates a tag protection for a repo
func CreateTagProtection(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/tag_protections repository repoCreateTagProtection
// ---
// summary: Create a tag protections for a repository
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateTagProtectionOption"
// responses:
// "201":
// "$ref": "#/responses/TagProtection"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
form := web.GetForm(ctx).(*api.CreateTagProtectionOption)
repo := ctx.Repo.Repository
namePattern := strings.TrimSpace(form.NamePattern)
if namePattern == "" {
ctx.Error(http.StatusBadRequest, "name_pattern are empty", "name_pattern are empty")
return
}
if len(form.WhitelistUsernames) == 0 && len(form.WhitelistTeams) == 0 {
ctx.Error(http.StatusBadRequest, "both whitelist_usernames and whitelist_teams are empty", "both whitelist_usernames and whitelist_teams are empty")
return
}
pt, err := git_model.GetProtectedTagByNamePattern(ctx, repo.ID, namePattern)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectTagOfRepo", err)
return
} else if pt != nil {
ctx.Error(http.StatusForbidden, "Create tag protection", "Tag protection already exist")
return
}
var whitelistUsers, whitelistTeams []int64
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
return
}
ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
return
}
if repo.Owner.IsOrganization() {
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.WhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
return
}
ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
return
}
}
protectTag := &git_model.ProtectedTag{
RepoID: repo.ID,
NamePattern: strings.TrimSpace(namePattern),
AllowlistUserIDs: whitelistUsers,
AllowlistTeamIDs: whitelistTeams,
}
if err := git_model.InsertProtectedTag(ctx, protectTag); err != nil {
ctx.Error(http.StatusInternalServerError, "InsertProtectedTag", err)
return
}
pt, err = git_model.GetProtectedTagByID(ctx, protectTag.ID)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err)
return
}
if pt == nil || pt.RepoID != repo.ID {
ctx.Error(http.StatusInternalServerError, "New tag protection not found", err)
return
}
ctx.JSON(http.StatusCreated, convert.ToTagProtection(ctx, pt, repo))
}
// EditTagProtection edits a tag protection for a repo
func EditTagProtection(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/tag_protections/{id} repository repoEditTagProtection
// ---
// summary: Edit a tag protections for a repository. Only fields that are set will be changed
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of protected tag
// type: integer
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditTagProtectionOption"
// responses:
// "200":
// "$ref": "#/responses/TagProtection"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
// "423":
// "$ref": "#/responses/repoArchivedError"
repo := ctx.Repo.Repository
form := web.GetForm(ctx).(*api.EditTagProtectionOption)
id := ctx.ParamsInt64(":id")
pt, err := git_model.GetProtectedTagByID(ctx, id)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err)
return
}
if pt == nil || pt.RepoID != repo.ID {
ctx.NotFound()
return
}
if form.NamePattern != nil {
pt.NamePattern = *form.NamePattern
}
var whitelistUsers, whitelistTeams []int64
if form.WhitelistTeams != nil {
if repo.Owner.IsOrganization() {
whitelistTeams, err = organization.GetTeamIDsByNames(ctx, repo.OwnerID, form.WhitelistTeams, false)
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
return
}
ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
return
}
}
pt.AllowlistTeamIDs = whitelistTeams
}
if form.WhitelistUsernames != nil {
whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.WhitelistUsernames, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
return
}
ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
return
}
pt.AllowlistUserIDs = whitelistUsers
}
err = git_model.UpdateProtectedTag(ctx, pt)
if err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateProtectedTag", err)
return
}
pt, err = git_model.GetProtectedTagByID(ctx, id)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err)
return
}
if pt == nil || pt.RepoID != repo.ID {
ctx.Error(http.StatusInternalServerError, "New tag protection not found", "New tag protection not found")
return
}
ctx.JSON(http.StatusOK, convert.ToTagProtection(ctx, pt, repo))
}
// DeleteTagProtection
func DeleteTagProtection(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/tag_protections/{id} repository repoDeleteTagProtection
// ---
// summary: Delete a specific tag protection for the repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: id
// in: path
// description: id of protected tag
// type: integer
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
repo := ctx.Repo.Repository
id := ctx.ParamsInt64(":id")
pt, err := git_model.GetProtectedTagByID(ctx, id)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetProtectedTagByID", err)
return
}
if pt == nil || pt.RepoID != repo.ID {
ctx.NotFound()
return
}
err = git_model.DeleteProtectedTag(ctx, pt)
if err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteProtectedTag", err)
return
}
ctx.Status(http.StatusNoContent)
}

View File

@ -184,6 +184,12 @@ type swaggerParameterBodies struct {
// in:body // in:body
CreateTagOption api.CreateTagOption CreateTagOption api.CreateTagOption
// in:body
CreateTagProtectionOption api.CreateTagProtectionOption
// in:body
EditTagProtectionOption api.EditTagProtectionOption
// in:body // in:body
CreateAccessTokenOption api.CreateAccessTokenOption CreateAccessTokenOption api.CreateAccessTokenOption

View File

@ -70,6 +70,20 @@ type swaggerResponseAnnotatedTag struct {
Body api.AnnotatedTag `json:"body"` Body api.AnnotatedTag `json:"body"`
} }
// TagProtectionList
// swagger:response TagProtectionList
type swaggerResponseTagProtectionList struct {
// in:body
Body []api.TagProtection `json:"body"`
}
// TagProtection
// swagger:response TagProtection
type swaggerResponseTagProtection struct {
// in:body
Body api.TagProtection `json:"body"`
}
// Reference // Reference
// swagger:response Reference // swagger:response Reference
type swaggerResponseReference struct { type swaggerResponseReference struct {

View File

@ -434,7 +434,7 @@ func preReceiveBranch(ctx *preReceiveContext, oldCommitID, newCommitID string, r
} }
} }
func preReceiveTag(ctx *preReceiveContext, oldCommitID, newCommitID string, refFullName git.RefName) { func preReceiveTag(ctx *preReceiveContext, oldCommitID, newCommitID string, refFullName git.RefName) { //nolint:unparam
if !ctx.AssertCanWriteCode() { if !ctx.AssertCanWriteCode() {
return return
} }
@ -470,7 +470,7 @@ func preReceiveTag(ctx *preReceiveContext, oldCommitID, newCommitID string, refF
} }
} }
func preReceiveFor(ctx *preReceiveContext, oldCommitID, newCommitID string, refFullName git.RefName) { func preReceiveFor(ctx *preReceiveContext, oldCommitID, newCommitID string, refFullName git.RefName) { //nolint:unparam
if !ctx.AssertCreatePullRequest() { if !ctx.AssertCreatePullRequest() {
return return
} }

View File

@ -183,7 +183,7 @@ func ChangeConfig(ctx *context.Context) {
value := ctx.FormString("value") value := ctx.FormString("value")
cfg := setting.Config() cfg := setting.Config()
marshalBool := func(v string) (string, error) { marshalBool := func(v string) (string, error) { //nolint:unparam
if b, _ := strconv.ParseBool(v); b { if b, _ := strconv.ParseBool(v); b {
return "true", nil return "true", nil
} }

View File

@ -6,6 +6,7 @@ package explore
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"strings"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
@ -57,47 +58,18 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) {
orderBy db.SearchOrderBy orderBy db.SearchOrderBy
) )
sortOrder := ctx.FormString("sort") sortOrder := strings.ToLower(ctx.FormString("sort"))
if sortOrder == "" { if sortOrder == "" {
sortOrder = setting.UI.ExploreDefaultSort sortOrder = setting.UI.ExploreDefaultSort
} }
ctx.Data["SortType"] = sortOrder
switch sortOrder { if order, ok := repo_model.OrderByFlatMap[sortOrder]; ok {
case "newest": orderBy = order
orderBy = db.SearchOrderByNewest } else {
case "oldest": sortOrder = "recentupdate"
orderBy = db.SearchOrderByOldest
case "leastupdate":
orderBy = db.SearchOrderByLeastUpdated
case "reversealphabetically":
orderBy = db.SearchOrderByAlphabeticallyReverse
case "alphabetically":
orderBy = db.SearchOrderByAlphabetically
case "reversesize":
orderBy = db.SearchOrderBySizeReverse
case "size":
orderBy = db.SearchOrderBySize
case "reversegitsize":
orderBy = db.SearchOrderByGitSizeReverse
case "gitsize":
orderBy = db.SearchOrderByGitSize
case "reverselfssize":
orderBy = db.SearchOrderByLFSSizeReverse
case "lfssize":
orderBy = db.SearchOrderByLFSSize
case "moststars":
orderBy = db.SearchOrderByStarsReverse
case "feweststars":
orderBy = db.SearchOrderByStars
case "mostforks":
orderBy = db.SearchOrderByForksReverse
case "fewestforks":
orderBy = db.SearchOrderByForks
default:
ctx.Data["SortType"] = "recentupdate"
orderBy = db.SearchOrderByRecentUpdated orderBy = db.SearchOrderByRecentUpdated
} }
ctx.Data["SortType"] = sortOrder
keyword := ctx.FormTrim("q") keyword := ctx.FormTrim("q")

View File

@ -39,7 +39,7 @@ func redirectToBadge(ctx *context_module.Context, label, text, color string) {
ctx.Redirect(getBadgeURL(ctx, label, text, color)) ctx.Redirect(getBadgeURL(ctx, label, text, color))
} }
func errorBadge(ctx *context_module.Context, label, text string) { func errorBadge(ctx *context_module.Context, label, text string) { //nolint:unparam
ctx.Redirect(getBadgeURL(ctx, label, text, "crimson")) ctx.Redirect(getBadgeURL(ctx, label, text, "crimson"))
} }

View File

@ -415,8 +415,9 @@ func RedirectDownload(ctx *context.Context) {
tagNames := []string{vTag} tagNames := []string{vTag}
curRepo := ctx.Repo.Repository curRepo := ctx.Repo.Repository
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{ releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
RepoID: curRepo.ID, IncludeDrafts: ctx.Repo.CanWrite(unit.TypeReleases),
TagNames: tagNames, RepoID: curRepo.ID,
TagNames: tagNames,
}) })
if err != nil { if err != nil {
ctx.ServerError("RedirectDownload", err) ctx.ServerError("RedirectDownload", err)
@ -628,7 +629,7 @@ func SearchRepo(ctx *context.Context) {
if len(sortOrder) == 0 { if len(sortOrder) == 0 {
sortOrder = "asc" sortOrder = "asc"
} }
if searchModeMap, ok := repo_model.SearchOrderByMap[sortOrder]; ok { if searchModeMap, ok := repo_model.OrderByMap[sortOrder]; ok {
if orderBy, ok := searchModeMap[sortMode]; ok { if orderBy, ok := searchModeMap[sortMode]; ok {
opts.OrderBy = orderBy opts.OrderBy = orderBy
} else { } else {

View File

@ -474,7 +474,7 @@ func handleSchedules(
detectedWorkflows []*actions_module.DetectedWorkflow, detectedWorkflows []*actions_module.DetectedWorkflow,
commit *git.Commit, commit *git.Commit,
input *notifyInput, input *notifyInput,
ref string, _ string,
) error { ) error {
branch, err := commit.GetBranchName() branch, err := commit.GetBranchName()
if err != nil { if err != nil {

View File

@ -411,6 +411,32 @@ func ToAnnotatedTagObject(repo *repo_model.Repository, commit *git.Commit) *api.
} }
} }
// ToTagProtection convert a git.ProtectedTag to an api.TagProtection
func ToTagProtection(ctx context.Context, pt *git_model.ProtectedTag, repo *repo_model.Repository) *api.TagProtection {
readers, err := access_model.GetRepoReaders(ctx, repo)
if err != nil {
log.Error("GetRepoReaders: %v", err)
}
whitelistUsernames := getWhitelistEntities(readers, pt.AllowlistUserIDs)
teamReaders, err := organization.OrgFromUser(repo.Owner).TeamsWithAccessToRepo(ctx, repo.ID, perm.AccessModeRead)
if err != nil {
log.Error("Repo.Owner.TeamsWithAccessToRepo: %v", err)
}
whitelistTeams := getWhitelistEntities(teamReaders, pt.AllowlistTeamIDs)
return &api.TagProtection{
ID: pt.ID,
NamePattern: pt.NamePattern,
WhitelistUsernames: whitelistUsernames,
WhitelistTeams: whitelistTeams,
Created: pt.CreatedUnix.AsTime(),
Updated: pt.UpdatedUnix.AsTime(),
}
}
// ToTopicResponse convert from models.Topic to api.TopicResponse // ToTopicResponse convert from models.Topic to api.TopicResponse
func ToTopicResponse(topic *repo_model.Topic) *api.TopicResponse { func ToTopicResponse(topic *repo_model.Topic) *api.TopicResponse {
return &api.TopicResponse{ return &api.TopicResponse{

View File

@ -92,7 +92,7 @@ func (o *repository) GetRepositoryPushURL() string {
return o.getURL() return o.getURL()
} }
func newRepository(ctx context.Context) generic.NodeDriverInterface { func newRepository(_ context.Context) generic.NodeDriverInterface {
r := &repository{ r := &repository{
f: &f3.Repository{}, f: &f3.Repository{},
} }

View File

@ -14,7 +14,7 @@ import (
"code.forgejo.org/f3/gof3/v3/options" "code.forgejo.org/f3/gof3/v3/options"
) )
func newTestOptions(t *testing.T) options.Interface { func newTestOptions(_ *testing.T) options.Interface {
o := options.GetFactory(driver_options.Name)().(*driver_options.Options) o := options.GetFactory(driver_options.Name)().(*driver_options.Options)
o.SetLogger(util.NewF3Logger(nil, forgejo_log.GetLogger(forgejo_log.DEFAULT))) o.SetLogger(util.NewF3Logger(nil, forgejo_log.GetLogger(forgejo_log.DEFAULT)))
return o return o

View File

@ -477,7 +477,7 @@ func buildObjectResponse(rc *requestContext, pointer lfs_module.Pointer, downloa
} }
// This is only needed to workaround https://github.com/git-lfs/git-lfs/issues/3662 // This is only needed to workaround https://github.com/git-lfs/git-lfs/issues/3662
verifyHeader["Accept"] = lfs_module.MediaType verifyHeader["Accept"] = lfs_module.AcceptHeader
rep.Actions["verify"] = &lfs_module.Link{Href: rc.VerifyLink(pointer), Header: verifyHeader} rep.Actions["verify"] = &lfs_module.Link{Href: rc.VerifyLink(pointer), Header: verifyHeader}
} }

View File

@ -45,7 +45,7 @@ func MailNewUser(ctx context.Context, u *user_model.User) {
} }
} }
func mailNewUser(ctx context.Context, u *user_model.User, lang string, tos []string) { func mailNewUser(_ context.Context, u *user_model.User, lang string, tos []string) {
locale := translation.NewLocale(lang) locale := translation.NewLocale(lang)
manageUserURL := setting.AppURL + "admin/users/" + strconv.FormatInt(u.ID, 10) manageUserURL := setting.AppURL + "admin/users/" + strconv.FormatInt(u.ID, 10)

View File

@ -252,7 +252,7 @@ func Merge(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.U
} }
// doMergeAndPush performs the merge operation without changing any pull information in database and pushes it up to the base repository // doMergeAndPush performs the merge operation without changing any pull information in database and pushes it up to the base repository
func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, pushTrigger repo_module.PushTrigger) (string, error) { func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, pushTrigger repo_module.PushTrigger) (string, error) { //nolint:unparam
// Clone base repo. // Clone base repo.
mergeCtx, cancel, err := createTemporaryRepoForMerge(ctx, pr, doer, expectedHeadCommitID) mergeCtx, cancel, err := createTemporaryRepoForMerge(ctx, pr, doer, expectedHeadCommitID)
if err != nil { if err != nil {

View File

@ -84,7 +84,7 @@ func MaybePromoteRemoteUser(ctx context.Context, source *auth_model.Source, logi
return true, reason, nil return true, reason, nil
} }
func getRemoteUserToPromote(ctx context.Context, source *auth_model.Source, loginName, email string) (*user_model.User, Reason, error) { func getRemoteUserToPromote(ctx context.Context, source *auth_model.Source, loginName, email string) (*user_model.User, Reason, error) { //nolint:unparam
if !source.IsOAuth2() { if !source.IsOAuth2() {
return nil, NewReason(log.DEBUG, ReasonNotAuth2, "source %v is not OAuth2", source), nil return nil, NewReason(log.DEBUG, ReasonNotAuth2, "source %v is not OAuth2", source), nil
} }

View File

@ -22,11 +22,11 @@
</div> </div>
</div> </div>
{{if .PackageDescriptor.Metadata.Description}} {{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.Comments}}
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.about"}}</h4> <h4 class="ui top attached header">{{ctx.Locale.Tr "packages.about"}}</h4>
<div class="ui attached segment"> {{if .PackageDescriptor.Metadata.Description}}<div class="ui attached segment">{{.PackageDescriptor.Metadata.Description}}</div>{{end}}
{{.PackageDescriptor.Metadata.Description}} {{if .PackageDescriptor.Metadata.Readme}}<div class="ui attached segment markup markdown">{{RenderMarkdownToHtml $.Context .PackageDescriptor.Metadata.Readme}}</div>{{end}}
</div> {{if .PackageDescriptor.Metadata.Comments}}<div class="ui attached segment">{{StringUtils.Join .PackageDescriptor.Metadata.Comments " "}}</div>{{end}}
{{end}} {{end}}
{{if or .PackageDescriptor.Metadata.Require .PackageDescriptor.Metadata.RequireDev}} {{if or .PackageDescriptor.Metadata.Require .PackageDescriptor.Metadata.RequireDev}}
@ -39,7 +39,7 @@
</div> </div>
{{end}} {{end}}
{{if or .PackageDescriptor.Metadata.Keywords}} {{if .PackageDescriptor.Metadata.Keywords}}
<h4 class="ui top attached header">{{ctx.Locale.Tr "packages.keywords"}}</h4> <h4 class="ui top attached header">{{ctx.Locale.Tr "packages.keywords"}}</h4>
<div class="ui attached segment"> <div class="ui attached segment">
{{range .PackageDescriptor.Metadata.Keywords}} {{range .PackageDescriptor.Metadata.Keywords}}

View File

@ -3616,7 +3616,7 @@
}, },
{ {
"type": "string", "type": "string",
"description": "sort repos by attribute. Supported values are \"alpha\", \"created\", \"updated\", \"size\", and \"id\". Default is \"alpha\"", "description": "sort repos by attribute. Supported values are \"alpha\", \"created\", \"updated\", \"size\", \"git_size\", \"lfs_size\", \"stars\", \"forks\" and \"id\". Default is \"alpha\"",
"name": "sort", "name": "sort",
"in": "query" "in": "query"
}, },
@ -14184,6 +14184,233 @@
} }
} }
}, },
"/repos/{owner}/{repo}/tag_protections": {
"get": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "List tag protections for a repository",
"operationId": "repoListTagProtection",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/TagProtectionList"
}
}
},
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Create a tag protections for a repository",
"operationId": "repoCreateTagProtection",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/CreateTagProtectionOption"
}
}
],
"responses": {
"201": {
"$ref": "#/responses/TagProtection"
},
"403": {
"$ref": "#/responses/forbidden"
},
"404": {
"$ref": "#/responses/notFound"
},
"422": {
"$ref": "#/responses/validationError"
},
"423": {
"$ref": "#/responses/repoArchivedError"
}
}
}
},
"/repos/{owner}/{repo}/tag_protections/{id}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Get a specific tag protection for the repository",
"operationId": "repoGetTagProtection",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "id of the tag protect to get",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/TagProtection"
},
"404": {
"$ref": "#/responses/notFound"
}
}
},
"delete": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Delete a specific tag protection for the repository",
"operationId": "repoDeleteTagProtection",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "id of protected tag",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"$ref": "#/responses/empty"
},
"404": {
"$ref": "#/responses/notFound"
}
}
},
"patch": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Edit a tag protections for a repository. Only fields that are set will be changed",
"operationId": "repoEditTagProtection",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "id of protected tag",
"name": "id",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/EditTagProtectionOption"
}
}
],
"responses": {
"200": {
"$ref": "#/responses/TagProtection"
},
"404": {
"$ref": "#/responses/notFound"
},
"422": {
"$ref": "#/responses/validationError"
},
"423": {
"$ref": "#/responses/repoArchivedError"
}
}
}
},
"/repos/{owner}/{repo}/tags": { "/repos/{owner}/{repo}/tags": {
"get": { "get": {
"produces": [ "produces": [
@ -20331,6 +20558,31 @@
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"
}, },
"CreateTagProtectionOption": {
"description": "CreateTagProtectionOption options for creating a tag protection",
"type": "object",
"properties": {
"name_pattern": {
"type": "string",
"x-go-name": "NamePattern"
},
"whitelist_teams": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "WhitelistTeams"
},
"whitelist_usernames": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "WhitelistUsernames"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"CreateTeamOption": { "CreateTeamOption": {
"description": "CreateTeamOption options for creating a team", "description": "CreateTeamOption options for creating a team",
"type": "object", "type": "object",
@ -21282,6 +21534,31 @@
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"
}, },
"EditTagProtectionOption": {
"description": "EditTagProtectionOption options for editing a tag protection",
"type": "object",
"properties": {
"name_pattern": {
"type": "string",
"x-go-name": "NamePattern"
},
"whitelist_teams": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "WhitelistTeams"
},
"whitelist_usernames": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "WhitelistUsernames"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"EditTeamOption": { "EditTeamOption": {
"description": "EditTeamOption options for editing a team", "description": "EditTeamOption options for editing a team",
"type": "object", "type": "object",
@ -24507,6 +24784,46 @@
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"
}, },
"TagProtection": {
"description": "TagProtection represents a tag protection",
"type": "object",
"properties": {
"created_at": {
"type": "string",
"format": "date-time",
"x-go-name": "Created"
},
"id": {
"type": "integer",
"format": "int64",
"x-go-name": "ID"
},
"name_pattern": {
"type": "string",
"x-go-name": "NamePattern"
},
"updated_at": {
"type": "string",
"format": "date-time",
"x-go-name": "Updated"
},
"whitelist_teams": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "WhitelistTeams"
},
"whitelist_usernames": {
"type": "array",
"items": {
"type": "string"
},
"x-go-name": "WhitelistUsernames"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"Team": { "Team": {
"description": "Team represents a team in an organization", "description": "Team represents a team in an organization",
"type": "object", "type": "object",
@ -26121,6 +26438,21 @@
} }
} }
}, },
"TagProtection": {
"description": "TagProtection",
"schema": {
"$ref": "#/definitions/TagProtection"
}
},
"TagProtectionList": {
"description": "TagProtectionList",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/TagProtection"
}
}
},
"TasksList": { "TasksList": {
"description": "TasksList", "description": "TasksList",
"schema": { "schema": {

View File

@ -105,7 +105,7 @@ func TestAPILFSLocksLogged(t *testing.T) {
for _, test := range tests { for _, test := range tests {
session := loginUser(t, test.user.Name) session := loginUser(t, test.user.Name)
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s.git/info/lfs/locks", test.repo.FullName()), map[string]string{"path": test.path}) req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s.git/info/lfs/locks", test.repo.FullName()), map[string]string{"path": test.path})
req.Header.Set("Accept", lfs.MediaType) req.Header.Set("Accept", lfs.AcceptHeader)
req.Header.Set("Content-Type", lfs.MediaType) req.Header.Set("Content-Type", lfs.MediaType)
resp := session.MakeRequest(t, req, test.httpResult) resp := session.MakeRequest(t, req, test.httpResult)
if len(test.addTime) > 0 { if len(test.addTime) > 0 {
@ -123,7 +123,7 @@ func TestAPILFSLocksLogged(t *testing.T) {
for _, test := range resultsTests { for _, test := range resultsTests {
session := loginUser(t, test.user.Name) session := loginUser(t, test.user.Name)
req := NewRequestf(t, "GET", "/%s.git/info/lfs/locks", test.repo.FullName()) req := NewRequestf(t, "GET", "/%s.git/info/lfs/locks", test.repo.FullName())
req.Header.Set("Accept", lfs.MediaType) req.Header.Set("Accept", lfs.AcceptHeader)
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
var lfsLocks api.LFSLockList var lfsLocks api.LFSLockList
DecodeJSON(t, resp, &lfsLocks) DecodeJSON(t, resp, &lfsLocks)
@ -135,7 +135,7 @@ func TestAPILFSLocksLogged(t *testing.T) {
} }
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s.git/info/lfs/locks/verify", test.repo.FullName()), map[string]string{}) req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s.git/info/lfs/locks/verify", test.repo.FullName()), map[string]string{})
req.Header.Set("Accept", lfs.MediaType) req.Header.Set("Accept", lfs.AcceptHeader)
req.Header.Set("Content-Type", lfs.MediaType) req.Header.Set("Content-Type", lfs.MediaType)
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
var lfsLocksVerify api.LFSLockListVerify var lfsLocksVerify api.LFSLockListVerify
@ -159,7 +159,7 @@ func TestAPILFSLocksLogged(t *testing.T) {
for _, test := range deleteTests { for _, test := range deleteTests {
session := loginUser(t, test.user.Name) session := loginUser(t, test.user.Name)
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s.git/info/lfs/locks/%s/unlock", test.repo.FullName(), test.lockID), map[string]string{}) req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s.git/info/lfs/locks/%s/unlock", test.repo.FullName(), test.lockID), map[string]string{})
req.Header.Set("Accept", lfs.MediaType) req.Header.Set("Accept", lfs.AcceptHeader)
req.Header.Set("Content-Type", lfs.MediaType) req.Header.Set("Content-Type", lfs.MediaType)
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
var lfsLockRep api.LFSLockResponse var lfsLockRep api.LFSLockResponse
@ -172,7 +172,7 @@ func TestAPILFSLocksLogged(t *testing.T) {
for _, test := range resultsTests { for _, test := range resultsTests {
session := loginUser(t, test.user.Name) session := loginUser(t, test.user.Name)
req := NewRequestf(t, "GET", "/%s.git/info/lfs/locks", test.repo.FullName()) req := NewRequestf(t, "GET", "/%s.git/info/lfs/locks", test.repo.FullName())
req.Header.Set("Accept", lfs.MediaType) req.Header.Set("Accept", lfs.AcceptHeader)
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
var lfsLocks api.LFSLockList var lfsLocks api.LFSLockList
DecodeJSON(t, resp, &lfsLocks) DecodeJSON(t, resp, &lfsLocks)

View File

@ -85,7 +85,7 @@ func TestAPILFSBatch(t *testing.T) {
newRequest := func(t testing.TB, br *lfs.BatchRequest) *RequestWrapper { newRequest := func(t testing.TB, br *lfs.BatchRequest) *RequestWrapper {
return NewRequestWithJSON(t, "POST", "/user2/lfs-batch-repo.git/info/lfs/objects/batch", br). return NewRequestWithJSON(t, "POST", "/user2/lfs-batch-repo.git/info/lfs/objects/batch", br).
SetHeader("Accept", lfs.MediaType). SetHeader("Accept", lfs.AcceptHeader).
SetHeader("Content-Type", lfs.MediaType) SetHeader("Content-Type", lfs.MediaType)
} }
decodeResponse := func(t *testing.T, b *bytes.Buffer) *lfs.BatchResponse { decodeResponse := func(t *testing.T, b *bytes.Buffer) *lfs.BatchResponse {
@ -448,7 +448,7 @@ func TestAPILFSVerify(t *testing.T) {
newRequest := func(t testing.TB, p *lfs.Pointer) *RequestWrapper { newRequest := func(t testing.TB, p *lfs.Pointer) *RequestWrapper {
return NewRequestWithJSON(t, "POST", "/user2/lfs-verify-repo.git/info/lfs/verify", p). return NewRequestWithJSON(t, "POST", "/user2/lfs-verify-repo.git/info/lfs/verify", p).
SetHeader("Accept", lfs.MediaType). SetHeader("Accept", lfs.AcceptHeader).
SetHeader("Content-Type", lfs.MediaType) SetHeader("Content-Type", lfs.MediaType)
} }