mirror of https://github.com/go-gitea/gitea
Merge branch 'main' into sync-issue-pr-and-more
This commit is contained in:
commit
775af9ef17
|
@ -81,7 +81,7 @@ docker run --entrypoint="" --rm -it gitea/act_runner:latest act_runner generate-
|
|||
When you are using the docker image, you can specify the configuration file by using the `CONFIG_FILE` environment variable. Make sure that the file is mounted into the container as a volume:
|
||||
|
||||
```bash
|
||||
docker run -v $(pwd)/config.yaml:/config.yaml -e CONFIG_FILE=/config.yaml ...
|
||||
docker run -v $PWD/config.yaml:/config.yaml -e CONFIG_FILE=/config.yaml ...
|
||||
```
|
||||
|
||||
You may notice the commands above are both incomplete, because it is not the time to run the act runner yet.
|
||||
|
@ -157,8 +157,8 @@ If you are using the docker image, behaviour will be slightly different. Registr
|
|||
|
||||
```bash
|
||||
docker run \
|
||||
-v $(pwd)/config.yaml:/config.yaml \
|
||||
-v $(pwd)/data:/data \
|
||||
-v $PWD/config.yaml:/config.yaml \
|
||||
-v $PWD/data:/data \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-e CONFIG_FILE=/config.yaml \
|
||||
-e GITEA_INSTANCE_URL=<instance_url> \
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
date: "2023-08-22T14:21:00+08:00"
|
||||
title: "Usage: Multi-factor Authentication (MFA)"
|
||||
slug: "multi-factor-authentication"
|
||||
weight: 15
|
||||
toc: false
|
||||
draft: false
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "usage"
|
||||
name: "Multi-factor Authentication (MFA)"
|
||||
weight: 15
|
||||
identifier: "multi-factor-authentication"
|
||||
---
|
||||
|
||||
# Multi-factor Authentication (MFA)
|
||||
|
||||
Multi-factor Authentication (also referred to as MFA or 2FA) enhances security by requiring a time-sensitive set of credentials in addition to a password.
|
||||
If a password were later to be compromised, logging into Gitea will not be possible without the additional credentials and the account would remain secure.
|
||||
Gitea supports both TOTP (Time-based One-Time Password) tokens and FIDO-based hardware keys using the Webauthn API.
|
||||
|
||||
MFA can be configured within the "Security" tab of the user settings page.
|
||||
|
||||
## MFA Considerations
|
||||
|
||||
Enabling MFA on a user does affect how the Git HTTP protocol can be used with the Git CLI.
|
||||
This interface does not support MFA, and trying to use a password normally will no longer be possible whilst MFA is enabled.
|
||||
If SSH is not an option for Git operations, an access token can be generated within the "Applications" tab of the user settings page.
|
||||
This access token can be used as if it were a password in order to allow the Git CLI to function over HTTP.
|
||||
|
||||
> **Warning** - By its very nature, an access token sidesteps the security benefits of MFA.
|
||||
> It must be kept secure and should only be used as a last resort.
|
||||
|
||||
The Gitea API supports providing the relevant TOTP password in the `X-Gitea-OTP` header, as described in [API Usage](development/api-usage.md).
|
||||
This should be used instead of an access token where possible.
|
2
go.mod
2
go.mod
|
@ -90,6 +90,7 @@ require (
|
|||
github.com/prometheus/client_golang v1.16.0
|
||||
github.com/quasoft/websspi v1.1.2
|
||||
github.com/redis/go-redis/v9 v9.0.5
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
|
||||
github.com/sassoftware/go-rpmutils v0.2.0
|
||||
github.com/sergi/go-diff v1.3.1
|
||||
|
@ -254,7 +255,6 @@ require (
|
|||
github.com/rhysd/actionlint v1.6.25 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/robfig/cron v1.2.0 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
// ActionSchedule represents a schedule of a workflow file
|
||||
type ActionSchedule struct {
|
||||
ID int64
|
||||
Title string
|
||||
Specs []string
|
||||
RepoID int64 `xorm:"index"`
|
||||
Repo *repo_model.Repository `xorm:"-"`
|
||||
OwnerID int64 `xorm:"index"`
|
||||
WorkflowID string
|
||||
TriggerUserID int64
|
||||
TriggerUser *user_model.User `xorm:"-"`
|
||||
Ref string
|
||||
CommitSHA string
|
||||
Event webhook_module.HookEventType
|
||||
EventPayload string `xorm:"LONGTEXT"`
|
||||
Content []byte
|
||||
Created timeutil.TimeStamp `xorm:"created"`
|
||||
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(ActionSchedule))
|
||||
}
|
||||
|
||||
// GetSchedulesMapByIDs returns the schedules by given id slice.
|
||||
func GetSchedulesMapByIDs(ids []int64) (map[int64]*ActionSchedule, error) {
|
||||
schedules := make(map[int64]*ActionSchedule, len(ids))
|
||||
return schedules, db.GetEngine(db.DefaultContext).In("id", ids).Find(&schedules)
|
||||
}
|
||||
|
||||
// GetReposMapByIDs returns the repos by given id slice.
|
||||
func GetReposMapByIDs(ids []int64) (map[int64]*repo_model.Repository, error) {
|
||||
repos := make(map[int64]*repo_model.Repository, len(ids))
|
||||
return repos, db.GetEngine(db.DefaultContext).In("id", ids).Find(&repos)
|
||||
}
|
||||
|
||||
var cronParser = cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
|
||||
|
||||
// CreateScheduleTask creates new schedule task.
|
||||
func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error {
|
||||
// Return early if there are no rows to insert
|
||||
if len(rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Begin transaction
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
// Loop through each schedule row
|
||||
for _, row := range rows {
|
||||
// Create new schedule row
|
||||
if err = db.Insert(ctx, row); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Loop through each schedule spec and create a new spec row
|
||||
now := time.Now()
|
||||
|
||||
for _, spec := range row.Specs {
|
||||
// Parse the spec and check for errors
|
||||
schedule, err := cronParser.Parse(spec)
|
||||
if err != nil {
|
||||
continue // skip to the next spec if there's an error
|
||||
}
|
||||
|
||||
// Insert the new schedule spec row
|
||||
if err = db.Insert(ctx, &ActionScheduleSpec{
|
||||
RepoID: row.RepoID,
|
||||
ScheduleID: row.ID,
|
||||
Spec: spec,
|
||||
Next: timeutil.TimeStamp(schedule.Next(now).Unix()),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
func DeleteScheduleTaskByRepo(ctx context.Context, id int64) error {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if _, err := db.GetEngine(ctx).Delete(&ActionSchedule{RepoID: id}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := db.GetEngine(ctx).Delete(&ActionScheduleSpec{RepoID: id}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type ScheduleList []*ActionSchedule
|
||||
|
||||
// GetUserIDs returns a slice of user's id
|
||||
func (schedules ScheduleList) GetUserIDs() []int64 {
|
||||
ids := make(container.Set[int64], len(schedules))
|
||||
for _, schedule := range schedules {
|
||||
ids.Add(schedule.TriggerUserID)
|
||||
}
|
||||
return ids.Values()
|
||||
}
|
||||
|
||||
func (schedules ScheduleList) GetRepoIDs() []int64 {
|
||||
ids := make(container.Set[int64], len(schedules))
|
||||
for _, schedule := range schedules {
|
||||
ids.Add(schedule.RepoID)
|
||||
}
|
||||
return ids.Values()
|
||||
}
|
||||
|
||||
func (schedules ScheduleList) LoadTriggerUser(ctx context.Context) error {
|
||||
userIDs := schedules.GetUserIDs()
|
||||
users := make(map[int64]*user_model.User, len(userIDs))
|
||||
if err := db.GetEngine(ctx).In("id", userIDs).Find(&users); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, schedule := range schedules {
|
||||
if schedule.TriggerUserID == user_model.ActionsUserID {
|
||||
schedule.TriggerUser = user_model.NewActionsUser()
|
||||
} else {
|
||||
schedule.TriggerUser = users[schedule.TriggerUserID]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (schedules ScheduleList) LoadRepos() error {
|
||||
repoIDs := schedules.GetRepoIDs()
|
||||
repos, err := repo_model.GetRepositoriesMapByIDs(repoIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, schedule := range schedules {
|
||||
schedule.Repo = repos[schedule.RepoID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type FindScheduleOptions struct {
|
||||
db.ListOptions
|
||||
RepoID int64
|
||||
OwnerID int64
|
||||
}
|
||||
|
||||
func (opts FindScheduleOptions) toConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.RepoID > 0 {
|
||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||
}
|
||||
if opts.OwnerID > 0 {
|
||||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
||||
}
|
||||
|
||||
return cond
|
||||
}
|
||||
|
||||
func FindSchedules(ctx context.Context, opts FindScheduleOptions) (ScheduleList, int64, error) {
|
||||
e := db.GetEngine(ctx).Where(opts.toConds())
|
||||
if !opts.ListAll && opts.PageSize > 0 && opts.Page >= 1 {
|
||||
e.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
|
||||
}
|
||||
var schedules ScheduleList
|
||||
total, err := e.Desc("id").FindAndCount(&schedules)
|
||||
return schedules, total, err
|
||||
}
|
||||
|
||||
func CountSchedules(ctx context.Context, opts FindScheduleOptions) (int64, error) {
|
||||
return db.GetEngine(ctx).Where(opts.toConds()).Count(new(ActionSchedule))
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
// ActionScheduleSpec represents a schedule spec of a workflow file
|
||||
type ActionScheduleSpec struct {
|
||||
ID int64
|
||||
RepoID int64 `xorm:"index"`
|
||||
Repo *repo_model.Repository `xorm:"-"`
|
||||
ScheduleID int64 `xorm:"index"`
|
||||
Schedule *ActionSchedule `xorm:"-"`
|
||||
|
||||
// Next time the job will run, or the zero time if Cron has not been
|
||||
// started or this entry's schedule is unsatisfiable
|
||||
Next timeutil.TimeStamp `xorm:"index"`
|
||||
// Prev is the last time this job was run, or the zero time if never.
|
||||
Prev timeutil.TimeStamp
|
||||
Spec string
|
||||
|
||||
Created timeutil.TimeStamp `xorm:"created"`
|
||||
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||
}
|
||||
|
||||
func (s *ActionScheduleSpec) Parse() (cron.Schedule, error) {
|
||||
return cronParser.Parse(s.Spec)
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(ActionScheduleSpec))
|
||||
}
|
||||
|
||||
func UpdateScheduleSpec(ctx context.Context, spec *ActionScheduleSpec, cols ...string) error {
|
||||
sess := db.GetEngine(ctx).ID(spec.ID)
|
||||
if len(cols) > 0 {
|
||||
sess.Cols(cols...)
|
||||
}
|
||||
_, err := sess.Update(spec)
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type SpecList []*ActionScheduleSpec
|
||||
|
||||
func (specs SpecList) GetScheduleIDs() []int64 {
|
||||
ids := make(container.Set[int64], len(specs))
|
||||
for _, spec := range specs {
|
||||
ids.Add(spec.ScheduleID)
|
||||
}
|
||||
return ids.Values()
|
||||
}
|
||||
|
||||
func (specs SpecList) LoadSchedules() error {
|
||||
scheduleIDs := specs.GetScheduleIDs()
|
||||
schedules, err := GetSchedulesMapByIDs(scheduleIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, spec := range specs {
|
||||
spec.Schedule = schedules[spec.ScheduleID]
|
||||
}
|
||||
|
||||
repoIDs := specs.GetRepoIDs()
|
||||
repos, err := GetReposMapByIDs(repoIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, spec := range specs {
|
||||
spec.Repo = repos[spec.RepoID]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (specs SpecList) GetRepoIDs() []int64 {
|
||||
ids := make(container.Set[int64], len(specs))
|
||||
for _, spec := range specs {
|
||||
ids.Add(spec.RepoID)
|
||||
}
|
||||
return ids.Values()
|
||||
}
|
||||
|
||||
func (specs SpecList) LoadRepos() error {
|
||||
repoIDs := specs.GetRepoIDs()
|
||||
repos, err := repo_model.GetRepositoriesMapByIDs(repoIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, spec := range specs {
|
||||
spec.Repo = repos[spec.RepoID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type FindSpecOptions struct {
|
||||
db.ListOptions
|
||||
RepoID int64
|
||||
Next int64
|
||||
}
|
||||
|
||||
func (opts FindSpecOptions) toConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.RepoID > 0 {
|
||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||
}
|
||||
|
||||
if opts.Next > 0 {
|
||||
cond = cond.And(builder.Lte{"next": opts.Next})
|
||||
}
|
||||
|
||||
return cond
|
||||
}
|
||||
|
||||
func FindSpecs(ctx context.Context, opts FindSpecOptions) (SpecList, int64, error) {
|
||||
e := db.GetEngine(ctx).Where(opts.toConds())
|
||||
if opts.PageSize > 0 && opts.Page >= 1 {
|
||||
e.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
|
||||
}
|
||||
var specs SpecList
|
||||
total, err := e.Desc("id").FindAndCount(&specs)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if err := specs.LoadSchedules(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return specs, total, nil
|
||||
}
|
||||
|
||||
func CountSpecs(ctx context.Context, opts FindSpecOptions) (int64, error) {
|
||||
return db.GetEngine(ctx).Where(opts.toConds()).Count(new(ActionScheduleSpec))
|
||||
}
|
|
@ -23,6 +23,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/references"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
|
@ -181,40 +182,32 @@ func (t CommentType) HasAttachmentSupport() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// RoleDescriptor defines comment tag type
|
||||
type RoleDescriptor int
|
||||
// RoleInRepo presents the user's participation in the repo
|
||||
type RoleInRepo string
|
||||
|
||||
// RoleDescriptor defines comment "role" tags
|
||||
type RoleDescriptor struct {
|
||||
IsPoster bool
|
||||
RoleInRepo RoleInRepo
|
||||
}
|
||||
|
||||
// Enumerate all the role tags.
|
||||
const (
|
||||
RoleDescriptorNone RoleDescriptor = iota
|
||||
RoleDescriptorPoster
|
||||
RoleDescriptorWriter
|
||||
RoleDescriptorOwner
|
||||
RoleRepoOwner RoleInRepo = "owner"
|
||||
RoleRepoMember RoleInRepo = "member"
|
||||
RoleRepoCollaborator RoleInRepo = "collaborator"
|
||||
RoleRepoFirstTimeContributor RoleInRepo = "first_time_contributor"
|
||||
RoleRepoContributor RoleInRepo = "contributor"
|
||||
)
|
||||
|
||||
// WithRole enable a specific tag on the RoleDescriptor.
|
||||
func (rd RoleDescriptor) WithRole(role RoleDescriptor) RoleDescriptor {
|
||||
return rd | (1 << role)
|
||||
// LocaleString returns the locale string name of the role
|
||||
func (r RoleInRepo) LocaleString(lang translation.Locale) string {
|
||||
return lang.Tr("repo.issues.role." + string(r))
|
||||
}
|
||||
|
||||
func stringToRoleDescriptor(role string) RoleDescriptor {
|
||||
switch role {
|
||||
case "Poster":
|
||||
return RoleDescriptorPoster
|
||||
case "Writer":
|
||||
return RoleDescriptorWriter
|
||||
case "Owner":
|
||||
return RoleDescriptorOwner
|
||||
default:
|
||||
return RoleDescriptorNone
|
||||
}
|
||||
}
|
||||
|
||||
// HasRole returns if a certain role is enabled on the RoleDescriptor.
|
||||
func (rd RoleDescriptor) HasRole(role string) bool {
|
||||
roleDescriptor := stringToRoleDescriptor(role)
|
||||
bitValue := rd & (1 << roleDescriptor)
|
||||
return (bitValue > 0)
|
||||
// LocaleHelper returns the locale tooltip of the role
|
||||
func (r RoleInRepo) LocaleHelper(lang translation.Locale) string {
|
||||
return lang.Tr("repo.issues.role." + string(r) + "_helper")
|
||||
}
|
||||
|
||||
// Comment represents a comment in commit and issue page.
|
||||
|
|
|
@ -115,10 +115,11 @@ func (l *Label) CalOpenIssues() {
|
|||
|
||||
// SetArchived set the label as archived
|
||||
func (l *Label) SetArchived(isArchived bool) {
|
||||
if isArchived && l.ArchivedUnix.IsZero() {
|
||||
l.ArchivedUnix = timeutil.TimeStampNow()
|
||||
} else {
|
||||
if !isArchived {
|
||||
l.ArchivedUnix = timeutil.TimeStamp(0)
|
||||
} else if isArchived && l.ArchivedUnix.IsZero() {
|
||||
// Only change the date when it is newly archived.
|
||||
l.ArchivedUnix = timeutil.TimeStampNow()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -199,3 +199,16 @@ func (prs PullRequestList) GetIssueIDs() []int64 {
|
|||
}
|
||||
return issueIDs
|
||||
}
|
||||
|
||||
// HasMergedPullRequestInRepo returns whether the user(poster) has merged pull-request in the repo
|
||||
func HasMergedPullRequestInRepo(ctx context.Context, repoID, posterID int64) (bool, error) {
|
||||
return db.GetEngine(ctx).
|
||||
Join("INNER", "pull_request", "pull_request.issue_id = issue.id").
|
||||
Where("repo_id=?", repoID).
|
||||
And("poster_id=?", posterID).
|
||||
And("is_pull=?", true).
|
||||
And("pull_request.has_merged=?", true).
|
||||
Select("issue.id").
|
||||
Limit(1).
|
||||
Get(new(Issue))
|
||||
}
|
||||
|
|
|
@ -526,6 +526,8 @@ var migrations = []Migration{
|
|||
NewMigration("Allow archiving labels", v1_21.AddArchivedUnixColumInLabelTable),
|
||||
// v272 -> v273
|
||||
NewMigration("Add Version to ActionRun table", v1_21.AddVersionToActionRunTable),
|
||||
// v273 -> v274
|
||||
NewMigration("Add Action Schedule Table", v1_21.AddActionScheduleTable),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current db version
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_21 //nolint
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddActionScheduleTable(x *xorm.Engine) error {
|
||||
type ActionSchedule struct {
|
||||
ID int64
|
||||
Title string
|
||||
Specs []string
|
||||
RepoID int64 `xorm:"index"`
|
||||
OwnerID int64 `xorm:"index"`
|
||||
WorkflowID string
|
||||
TriggerUserID int64
|
||||
Ref string
|
||||
CommitSHA string
|
||||
Event string
|
||||
EventPayload string `xorm:"LONGTEXT"`
|
||||
Content []byte
|
||||
Created timeutil.TimeStamp `xorm:"created"`
|
||||
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||
}
|
||||
|
||||
type ActionScheduleSpec struct {
|
||||
ID int64
|
||||
RepoID int64 `xorm:"index"`
|
||||
ScheduleID int64 `xorm:"index"`
|
||||
Spec string
|
||||
Next timeutil.TimeStamp `xorm:"index"`
|
||||
Prev timeutil.TimeStamp
|
||||
|
||||
Created timeutil.TimeStamp `xorm:"created"`
|
||||
Updated timeutil.TimeStamp `xorm:"updated"`
|
||||
}
|
||||
|
||||
return x.Sync(
|
||||
new(ActionSchedule),
|
||||
new(ActionScheduleSpec),
|
||||
)
|
||||
}
|
|
@ -170,6 +170,8 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error {
|
|||
&actions_model.ActionRunJob{RepoID: repoID},
|
||||
&actions_model.ActionRun{RepoID: repoID},
|
||||
&actions_model.ActionRunner{RepoID: repoID},
|
||||
&actions_model.ActionScheduleSpec{RepoID: repoID},
|
||||
&actions_model.ActionSchedule{RepoID: repoID},
|
||||
&actions_model.ActionArtifact{RepoID: repoID},
|
||||
); err != nil {
|
||||
return fmt.Errorf("deleteBeans: %w", err)
|
||||
|
|
|
@ -133,6 +133,11 @@ func (r *Release) HTMLURL() string {
|
|||
return r.Repo.HTMLURL() + "/releases/tag/" + util.PathEscapeSegments(r.TagName)
|
||||
}
|
||||
|
||||
// APIUploadURL the api url to upload assets to a release. release must have attributes loaded
|
||||
func (r *Release) APIUploadURL() string {
|
||||
return r.APIURL() + "/assets"
|
||||
}
|
||||
|
||||
// Link the relative url for a release on the web UI. release must have attributes loaded
|
||||
func (r *Release) Link() string {
|
||||
return r.Repo.Link() + "/releases/tag/" + util.PathEscapeSegments(r.TagName)
|
||||
|
|
|
@ -130,6 +130,10 @@ type SearchRepoOptions struct {
|
|||
// True -> include just collaborative
|
||||
// False -> include just non-collaborative
|
||||
Collaborate util.OptionalBool
|
||||
// What type of unit the user can be collaborative in,
|
||||
// it is ignored if Collaborate is False.
|
||||
// TypeInvalid means any unit type.
|
||||
UnitType unit.Type
|
||||
// None -> include forks AND non-forks
|
||||
// True -> include just forks
|
||||
// False -> include just non-forks
|
||||
|
@ -382,19 +386,25 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
|
|||
|
||||
if opts.Collaborate != util.OptionalBoolFalse {
|
||||
// A Collaboration is:
|
||||
collaborateCond := builder.And(
|
||||
// 1. Repository we don't own
|
||||
builder.Neq{"owner_id": opts.OwnerID},
|
||||
// 2. But we can see because of:
|
||||
builder.Or(
|
||||
// A. We have unit independent access
|
||||
UserAccessRepoCond("`repository`.id", opts.OwnerID),
|
||||
// B. We are in a team for
|
||||
UserOrgTeamRepoCond("`repository`.id", opts.OwnerID),
|
||||
// C. Public repositories in organizations that we are member of
|
||||
userOrgPublicRepoCondPrivate(opts.OwnerID),
|
||||
),
|
||||
)
|
||||
|
||||
collaborateCond := builder.NewCond()
|
||||
// 1. Repository we don't own
|
||||
collaborateCond = collaborateCond.And(builder.Neq{"owner_id": opts.OwnerID})
|
||||
// 2. But we can see because of:
|
||||
{
|
||||
userAccessCond := builder.NewCond()
|
||||
// A. We have unit independent access
|
||||
userAccessCond = userAccessCond.Or(UserAccessRepoCond("`repository`.id", opts.OwnerID))
|
||||
// B. We are in a team for
|
||||
if opts.UnitType == unit.TypeInvalid {
|
||||
userAccessCond = userAccessCond.Or(UserOrgTeamRepoCond("`repository`.id", opts.OwnerID))
|
||||
} else {
|
||||
userAccessCond = userAccessCond.Or(userOrgTeamUnitRepoCond("`repository`.id", opts.OwnerID, opts.UnitType))
|
||||
}
|
||||
// C. Public repositories in organizations that we are member of
|
||||
userAccessCond = userAccessCond.Or(userOrgPublicRepoCondPrivate(opts.OwnerID))
|
||||
collaborateCond = collaborateCond.And(userAccessCond)
|
||||
}
|
||||
if !opts.Private {
|
||||
collaborateCond = collaborateCond.And(builder.Expr("owner_id NOT IN (SELECT org_id FROM org_user WHERE org_user.uid = ? AND org_user.is_public = ?)", opts.OwnerID, false))
|
||||
}
|
||||
|
|
|
@ -6,12 +6,14 @@ package secret
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
secret_module "code.gitea.io/gitea/modules/secret"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
@ -26,6 +28,25 @@ type Secret struct {
|
|||
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
|
||||
}
|
||||
|
||||
// ErrSecretNotFound represents a "secret not found" error.
|
||||
type ErrSecretNotFound struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// IsErrSecretNotFound checks if an error is a ErrSecretNotFound.
|
||||
func IsErrSecretNotFound(err error) bool {
|
||||
_, ok := err.(ErrSecretNotFound)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrSecretNotFound) Error() string {
|
||||
return fmt.Sprintf("secret was not found [name: %s]", err.Name)
|
||||
}
|
||||
|
||||
func (err ErrSecretNotFound) Unwrap() error {
|
||||
return util.ErrNotExist
|
||||
}
|
||||
|
||||
// newSecret Creates a new already encrypted secret
|
||||
func newSecret(ownerID, repoID int64, name, data string) *Secret {
|
||||
return &Secret{
|
||||
|
@ -93,3 +114,49 @@ func FindSecrets(ctx context.Context, opts FindSecretsOptions) ([]*Secret, error
|
|||
func CountSecrets(ctx context.Context, opts *FindSecretsOptions) (int64, error) {
|
||||
return db.GetEngine(ctx).Where(opts.toConds()).Count(new(Secret))
|
||||
}
|
||||
|
||||
// UpdateSecret changes org or user reop secret.
|
||||
func UpdateSecret(ctx context.Context, orgID, repoID int64, name, data string) error {
|
||||
sc := new(Secret)
|
||||
name = strings.ToUpper(name)
|
||||
has, err := db.GetEngine(ctx).
|
||||
Where("owner_id=?", orgID).
|
||||
And("repo_id=?", repoID).
|
||||
And("name=?", name).
|
||||
Get(sc)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return ErrSecretNotFound{Name: name}
|
||||
}
|
||||
|
||||
encrypted, err := secret_module.EncryptSecret(setting.SecretKey, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sc.Data = encrypted
|
||||
_, err = db.GetEngine(ctx).ID(sc.ID).Cols("data").Update(sc)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteSecret deletes secret from an organization.
|
||||
func DeleteSecret(ctx context.Context, orgID, repoID int64, name string) error {
|
||||
sc := new(Secret)
|
||||
has, err := db.GetEngine(ctx).
|
||||
Where("owner_id=?", orgID).
|
||||
And("repo_id=?", repoID).
|
||||
And("name=?", strings.ToUpper(name)).
|
||||
Get(sc)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return ErrSecretNotFound{Name: name}
|
||||
}
|
||||
|
||||
if _, err := db.GetEngine(ctx).ID(sc.ID).Delete(new(Secret)); err != nil {
|
||||
return fmt.Errorf("Delete: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -95,18 +95,25 @@ func GetEventsFromContent(content []byte) ([]*jobparser.Event, error) {
|
|||
return events, nil
|
||||
}
|
||||
|
||||
func DetectWorkflows(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader) ([]*DetectedWorkflow, error) {
|
||||
func DetectWorkflows(
|
||||
gitRepo *git.Repository,
|
||||
commit *git.Commit,
|
||||
triggedEvent webhook_module.HookEventType,
|
||||
payload api.Payloader,
|
||||
) ([]*DetectedWorkflow, []*DetectedWorkflow, error) {
|
||||
entries, err := ListWorkflows(commit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
workflows := make([]*DetectedWorkflow, 0, len(entries))
|
||||
schedules := make([]*DetectedWorkflow, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
content, err := GetContentFromEntry(entry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
events, err := GetEventsFromContent(content)
|
||||
if err != nil {
|
||||
log.Warn("ignore invalid workflow %q: %v", entry.Name(), err)
|
||||
|
@ -114,6 +121,14 @@ func DetectWorkflows(gitRepo *git.Repository, commit *git.Commit, triggedEvent w
|
|||
}
|
||||
for _, evt := range events {
|
||||
log.Trace("detect workflow %q for event %#v matching %q", entry.Name(), evt, triggedEvent)
|
||||
if evt.IsSchedule() {
|
||||
dwf := &DetectedWorkflow{
|
||||
EntryName: entry.Name(),
|
||||
TriggerEvent: evt.Name,
|
||||
Content: content,
|
||||
}
|
||||
schedules = append(schedules, dwf)
|
||||
}
|
||||
if detectMatched(gitRepo, commit, triggedEvent, payload, evt) {
|
||||
dwf := &DetectedWorkflow{
|
||||
EntryName: entry.Name(),
|
||||
|
@ -125,7 +140,7 @@ func DetectWorkflows(gitRepo *git.Repository, commit *git.Commit, triggedEvent w
|
|||
}
|
||||
}
|
||||
|
||||
return workflows, nil
|
||||
return workflows, schedules, nil
|
||||
}
|
||||
|
||||
func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool {
|
||||
|
|
|
@ -495,7 +495,7 @@ func GetCommitFileStatus(ctx context.Context, repoPath, commitID string) (*Commi
|
|||
}()
|
||||
|
||||
stderr := new(bytes.Buffer)
|
||||
err := NewCommand(ctx, "log", "--name-status", "-c", "--pretty=format:", "--parents", "--no-renames", "-z", "-1").AddDynamicArguments(commitID).Run(&RunOpts{
|
||||
err := NewCommand(ctx, "log", "--name-status", "-m", "--pretty=format:", "--first-parent", "--no-renames", "-z", "-1").AddDynamicArguments(commitID).Run(&RunOpts{
|
||||
Dir: repoPath,
|
||||
Stdout: w,
|
||||
Stderr: stderr,
|
||||
|
|
|
@ -255,3 +255,26 @@ func TestParseCommitFileStatus(t *testing.T) {
|
|||
assert.Equal(t, kase.modified, fileStatus.Modified)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCommitFileStatusMerges(t *testing.T) {
|
||||
bareRepo1Path := filepath.Join(testReposDir, "repo6_merge")
|
||||
|
||||
commitFileStatus, err := GetCommitFileStatus(DefaultContext, bareRepo1Path, "022f4ce6214973e018f02bf363bf8a2e3691f699")
|
||||
assert.NoError(t, err)
|
||||
|
||||
expected := CommitFileStatus{
|
||||
[]string{
|
||||
"add_file.txt",
|
||||
},
|
||||
[]string{
|
||||
"to_remove.txt",
|
||||
},
|
||||
[]string{
|
||||
"to_modify.txt",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, commitFileStatus.Added, expected.Added)
|
||||
assert.Equal(t, commitFileStatus.Removed, expected.Removed)
|
||||
assert.Equal(t, commitFileStatus.Modified, expected.Modified)
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
ref: refs/heads/main
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,2 @@
|
|||
x<01>ΓM
|
||||
1β aΓ=EΓΆβ$Β΅ΓLAΓ«ô'Γβ‘βiæþRΒ½β¬βΊgΓ·~_ΓΓu1Β ΛNΒΆβΉ@mZ)g<>2β¦Dβjβ’Ε *_βfΕΓsΒ₯4Γ―Γ²aΓmβnp>βΓΛΓ§β¬Γ>!ΕΓ#ΒΊΓΊΒ½1ΓΉ;p]Γ¬xΓΏΓ¦uyIwΓN4Γ
|
|
@ -0,0 +1,2 @@
|
|||
xβΠ½M
|
||||
Π±0βaΠ²9Π΅Πβ₯ΠM2 ΠUΒ Π€ββ=Β©Ρβ¦yv/βͺΠ‘β€βΠΊ Π³Π0:@β Π $β βUΡββ.β[ΠΉ>β β lβ βIΠ»sΠxΠ βΠ8'TββRβ§Π΄βS,ββ $x.
ΠΆΒ Π=n[ββΠ²βΠ‘ΠuΠβ’s!+9ββ¬ΠBGvΠ»fΠ§m
Πβ€βuΠ¨βΠ·rβ€Π«ΠͺΠ°>β
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,5 @@
|
|||
xΕ€ΓM
|
||||
1@aΓ=EΓΆβΒ€?iΕ*mΕ‘ΓΓββ’xOΕΔΕ°=xΛ.ΓlΕβ’OΒΆΒ©RΛZ80Ε Γ\[Γ*Γ%Β΅b
|
||||
<EFBFBD>&qΔ¢éà βIΕ Ε½ΔΕ‘ΓΓ§<C394>Ε₯
|
||||
7ΔΔΓβΉFΔΒ¨ΕΔ½wΓcΕΉuΓΓΕzx?ΒΈΓΕΓ§Ε‘0Γ§
|
||||
Ε1 :Ε―mβ’Ε£ΒΈ6LΓ³SΓΕΓ>&
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
022f4ce6214973e018f02bf363bf8a2e3691f699
|
|
@ -0,0 +1 @@
|
|||
ae4b035e7c4afbc000576cee3f713ea0c2f1e1e2
|
|
@ -0,0 +1 @@
|
|||
d1792641396ff7630d35fbb0b74b86b0c71bca77
|
|
@ -0,0 +1 @@
|
|||
38ec3e0cdc88bde01014bda4a5dd9fc835f41439
|
|
@ -13,6 +13,7 @@ import (
|
|||
|
||||
db_model "code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/indexer/issues/bleve"
|
||||
"code.gitea.io/gitea/modules/indexer/issues/db"
|
||||
|
@ -277,7 +278,7 @@ func IsAvailable(ctx context.Context) bool {
|
|||
}
|
||||
|
||||
// SearchOptions indicates the options for searching issues
|
||||
type SearchOptions internal.SearchOptions
|
||||
type SearchOptions = internal.SearchOptions
|
||||
|
||||
const (
|
||||
SortByCreatedDesc = internal.SortByCreatedDesc
|
||||
|
@ -291,7 +292,6 @@ const (
|
|||
)
|
||||
|
||||
// SearchIssues search issues by options.
|
||||
// It returns issue ids and a bool value indicates if the result is imprecise.
|
||||
func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, error) {
|
||||
indexer := *globalIndexer.Load()
|
||||
|
||||
|
@ -305,7 +305,7 @@ func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, err
|
|||
indexer = db.NewIndexer()
|
||||
}
|
||||
|
||||
result, err := indexer.Search(ctx, (*internal.SearchOptions)(opts))
|
||||
result, err := indexer.Search(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
@ -317,3 +317,38 @@ func SearchIssues(ctx context.Context, opts *SearchOptions) ([]int64, int64, err
|
|||
|
||||
return ret, result.Total, nil
|
||||
}
|
||||
|
||||
// CountIssues counts issues by options. It is a shortcut of SearchIssues(ctx, opts) but only returns the total count.
|
||||
func CountIssues(ctx context.Context, opts *SearchOptions) (int64, error) {
|
||||
opts = opts.Copy(func(options *SearchOptions) { opts.Paginator = &db_model.ListOptions{PageSize: 0} })
|
||||
|
||||
_, total, err := SearchIssues(ctx, opts)
|
||||
return total, err
|
||||
}
|
||||
|
||||
// CountIssuesByRepo counts issues by options and group by repo id.
|
||||
// It's not a complete implementation, since it requires the caller should provide the repo ids.
|
||||
// That means opts.RepoIDs must be specified, and opts.AllPublic must be false.
|
||||
// It's good enough for the current usage, and it can be improved if needed.
|
||||
// TODO: use "group by" of the indexer engines to implement it.
|
||||
func CountIssuesByRepo(ctx context.Context, opts *SearchOptions) (map[int64]int64, error) {
|
||||
if len(opts.RepoIDs) == 0 {
|
||||
return nil, fmt.Errorf("opts.RepoIDs must be specified")
|
||||
}
|
||||
if opts.AllPublic {
|
||||
return nil, fmt.Errorf("opts.AllPublic must be false")
|
||||
}
|
||||
|
||||
repoIDs := container.SetOf(opts.RepoIDs...).Values()
|
||||
ret := make(map[int64]int64, len(repoIDs))
|
||||
// TODO: it could be faster if do it in parallel for some indexer engines. Improve it if users report it's slow.
|
||||
for _, repoID := range repoIDs {
|
||||
count, err := CountIssues(ctx, opts.Copy(func(o *internal.SearchOptions) { o.RepoIDs = []int64{repoID} }))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret[repoID] = count
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
|
|
@ -109,6 +109,19 @@ type SearchOptions struct {
|
|||
SortBy SortBy // sort by field
|
||||
}
|
||||
|
||||
// Copy returns a copy of the options.
|
||||
// Be careful, it's not a deep copy, so `SearchOptions.RepoIDs = {...}` is OK while `SearchOptions.RepoIDs[0] = ...` is not.
|
||||
func (o *SearchOptions) Copy(edit ...func(options *SearchOptions)) *SearchOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
v := *o
|
||||
for _, e := range edit {
|
||||
e(&v)
|
||||
}
|
||||
return &v
|
||||
}
|
||||
|
||||
type SortBy string
|
||||
|
||||
const (
|
||||
|
|
|
@ -18,6 +18,7 @@ type Release struct {
|
|||
HTMLURL string `json:"html_url"`
|
||||
TarURL string `json:"tarball_url"`
|
||||
ZipURL string `json:"zipball_url"`
|
||||
UploadURL string `json:"upload_url"`
|
||||
IsDraft bool `json:"draft"`
|
||||
IsPrerelease bool `json:"prerelease"`
|
||||
// swagger:strfmt date-time
|
||||
|
|
|
@ -25,3 +25,12 @@ type CreateSecretOption struct {
|
|||
// Data of the secret to create
|
||||
Data string `json:"data" binding:"Required"`
|
||||
}
|
||||
|
||||
// UpdateSecretOption options when updating secret
|
||||
// swagger:model
|
||||
type UpdateSecretOption struct {
|
||||
// Data of the secret to update
|
||||
//
|
||||
// required: true
|
||||
Data string `json:"data" binding:"Required"`
|
||||
}
|
||||
|
|
|
@ -1480,9 +1480,18 @@ issues.ref_reopening_from = `<a href="%[3]s">referenced a pull request %[4]s tha
|
|||
issues.ref_closed_from = `<a href="%[3]s">closed this issue %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.ref_reopened_from = `<a href="%[3]s">reopened this issue %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.ref_from = `from %[1]s`
|
||||
issues.poster = Poster
|
||||
issues.collaborator = Collaborator
|
||||
issues.owner = Owner
|
||||
issues.author = Author
|
||||
issues.author_helper = This user is the author.
|
||||
issues.role.owner = Owner
|
||||
issues.role.owner_helper = This user is the owner of this repository.
|
||||
issues.role.member = Member
|
||||
issues.role.member_helper = This user is a member of the organization owning this repository.
|
||||
issues.role.collaborator = Collaborator
|
||||
issues.role.collaborator_helper = This user has been invited to collaborate on the repository.
|
||||
issues.role.first_time_contributor = First-time contributor
|
||||
issues.role.first_time_contributor_helper = This is the first contribution of this user to the repository.
|
||||
issues.role.contributor = Contributor
|
||||
issues.role.contributor_helper = This user has previously committed to the repository.
|
||||
issues.re_request_review=Re-request review
|
||||
issues.is_stale = There have been changes to this PR since this review
|
||||
issues.remove_request_review=Remove review request
|
||||
|
@ -2756,6 +2765,7 @@ dashboard.gc_lfs = Garbage collect LFS meta objects
|
|||
dashboard.stop_zombie_tasks = Stop zombie tasks
|
||||
dashboard.stop_endless_tasks = Stop endless tasks
|
||||
dashboard.cancel_abandoned_jobs = Cancel abandoned jobs
|
||||
dashboard.start_schedule_tasks = Start schedule tasks
|
||||
dashboard.sync_branch.started = Branches Sync started
|
||||
dashboard.rebuild_issue_indexer = Rebuild issue indexer
|
||||
|
||||
|
|
|
@ -1301,6 +1301,9 @@ func Routes() *web.Route {
|
|||
m.Group("/actions/secrets", func() {
|
||||
m.Get("", reqToken(), reqOrgOwnership(), org.ListActionsSecrets)
|
||||
m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateSecretOption{}), org.CreateOrgSecret)
|
||||
m.Combo("/{secretname}").
|
||||
Put(reqToken(), reqOrgOwnership(), bind(api.UpdateSecretOption{}), org.UpdateOrgSecret).
|
||||
Delete(reqToken(), reqOrgOwnership(), org.DeleteOrgSecret)
|
||||
})
|
||||
m.Group("/public_members", func() {
|
||||
m.Get("", org.ListPublicMembers)
|
||||
|
|
|
@ -103,6 +103,10 @@ func CreateOrgSecret(ctx *context.APIContext) {
|
|||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
opt := web.GetForm(ctx).(*api.CreateSecretOption)
|
||||
if err := actions.NameRegexMatch(opt.Name); err != nil {
|
||||
ctx.Error(http.StatusBadRequest, "CreateOrgSecret", err)
|
||||
return
|
||||
}
|
||||
s, err := secret_model.InsertEncryptedSecret(
|
||||
ctx, ctx.Org.Organization.ID, 0, opt.Name, actions.ReserveLineBreakForTextarea(opt.Data),
|
||||
)
|
||||
|
@ -113,3 +117,90 @@ func CreateOrgSecret(ctx *context.APIContext) {
|
|||
|
||||
ctx.JSON(http.StatusCreated, convert.ToSecret(s))
|
||||
}
|
||||
|
||||
// UpdateOrgSecret update one secret of the organization
|
||||
func UpdateOrgSecret(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /orgs/{org}/actions/secrets/{secretname} organization updateOrgSecret
|
||||
// ---
|
||||
// summary: Update a secret value in an organization
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: secretname
|
||||
// in: path
|
||||
// description: name of the secret
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/UpdateSecretOption"
|
||||
// responses:
|
||||
// "204":
|
||||
// description: update one secret of the organization
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
secretName := ctx.Params(":secretname")
|
||||
opt := web.GetForm(ctx).(*api.UpdateSecretOption)
|
||||
err := secret_model.UpdateSecret(
|
||||
ctx, ctx.Org.Organization.ID, 0, secretName, opt.Data,
|
||||
)
|
||||
if secret_model.IsErrSecretNotFound(err) {
|
||||
ctx.NotFound(err)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateSecret", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// DeleteOrgSecret delete one secret of the organization
|
||||
func DeleteOrgSecret(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /orgs/{org}/actions/secrets/{secretname} organization deleteOrgSecret
|
||||
// ---
|
||||
// summary: Delete a secret in an organization
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: secretname
|
||||
// in: path
|
||||
// description: name of the secret
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// description: delete one secret of the organization
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
secretName := ctx.Params(":secretname")
|
||||
err := secret_model.DeleteSecret(
|
||||
ctx, ctx.Org.Organization.ID, 0, secretName,
|
||||
)
|
||||
if secret_model.IsErrSecretNotFound(err) {
|
||||
ctx.NotFound(err)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "DeleteSecret", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
|
|
@ -768,7 +768,8 @@ func EditBranchProtection(ctx *context.APIContext) {
|
|||
if form.EnableStatusCheck != nil {
|
||||
protectBranch.EnableStatusCheck = *form.EnableStatusCheck
|
||||
}
|
||||
if protectBranch.EnableStatusCheck {
|
||||
|
||||
if form.StatusCheckContexts != nil {
|
||||
protectBranch.StatusCheckContexts = form.StatusCheckContexts
|
||||
}
|
||||
|
||||
|
|
|
@ -190,4 +190,7 @@ type swaggerParameterBodies struct {
|
|||
|
||||
// in:body
|
||||
CreateSecretOption api.CreateSecretOption
|
||||
|
||||
// in:body
|
||||
UpdateSecretOption api.UpdateSecretOption
|
||||
}
|
||||
|
|
|
@ -480,5 +480,5 @@ func DeleteAvatar(ctx *context.Context) {
|
|||
ctx.Flash.Error(err.Error())
|
||||
}
|
||||
|
||||
ctx.Redirect(setting.AppSubURL + "/admin/users/" + strconv.FormatInt(u.ID, 10))
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/admin/users/" + strconv.FormatInt(u.ID, 10))
|
||||
}
|
||||
|
|
|
@ -156,7 +156,7 @@ func SettingsDeleteAvatar(ctx *context.Context) {
|
|||
ctx.Flash.Error(err.Error())
|
||||
}
|
||||
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/settings")
|
||||
ctx.JSONRedirect(ctx.Org.OrgLink + "/settings")
|
||||
}
|
||||
|
||||
// SettingsDelete response for deleting an organization
|
||||
|
|
|
@ -81,7 +81,6 @@ func Commits(ctx *context.Context) {
|
|||
ctx.Data["Username"] = ctx.Repo.Owner.Name
|
||||
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
|
||||
ctx.Data["CommitCount"] = commitsCount
|
||||
ctx.Data["RefName"] = ctx.Repo.RefName
|
||||
|
||||
pager := context.NewPagination(int(commitsCount), pageSize, page, 5)
|
||||
pager.SetDefaultParams(ctx)
|
||||
|
@ -157,7 +156,7 @@ func Graph(ctx *context.Context) {
|
|||
ctx.Data["Username"] = ctx.Repo.Owner.Name
|
||||
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
|
||||
ctx.Data["CommitCount"] = commitsCount
|
||||
ctx.Data["RefName"] = ctx.Repo.RefName
|
||||
|
||||
paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5)
|
||||
paginator.AddParam(ctx, "mode", "Mode")
|
||||
paginator.AddParam(ctx, "hide-pr-refs", "HidePRRefs")
|
||||
|
@ -203,7 +202,6 @@ func SearchCommits(ctx *context.Context) {
|
|||
}
|
||||
ctx.Data["Username"] = ctx.Repo.Owner.Name
|
||||
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
|
||||
ctx.Data["RefName"] = ctx.Repo.RefName
|
||||
ctx.HTML(http.StatusOK, tplCommits)
|
||||
}
|
||||
|
||||
|
@ -247,7 +245,6 @@ func FileHistory(ctx *context.Context) {
|
|||
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
|
||||
ctx.Data["FileName"] = fileName
|
||||
ctx.Data["CommitCount"] = commitsCount
|
||||
ctx.Data["RefName"] = ctx.Repo.RefName
|
||||
|
||||
pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5)
|
||||
pager.SetDefaultParams(ctx)
|
||||
|
|
|
@ -1228,47 +1228,70 @@ func NewIssuePost(ctx *context.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
// roleDescriptor returns the Role Descriptor for a comment in/with the given repo, poster and issue
|
||||
// roleDescriptor returns the role descriptor for a comment in/with the given repo, poster and issue
|
||||
func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *user_model.User, issue *issues_model.Issue, hasOriginalAuthor bool) (issues_model.RoleDescriptor, error) {
|
||||
roleDescriptor := issues_model.RoleDescriptor{}
|
||||
|
||||
if hasOriginalAuthor {
|
||||
return issues_model.RoleDescriptorNone, nil
|
||||
return roleDescriptor, nil
|
||||
}
|
||||
|
||||
perm, err := access_model.GetUserRepoPermission(ctx, repo, poster)
|
||||
if err != nil {
|
||||
return issues_model.RoleDescriptorNone, err
|
||||
}
|
||||
|
||||
// By default the poster has no roles on the comment.
|
||||
roleDescriptor := issues_model.RoleDescriptorNone
|
||||
|
||||
// Check if the poster is owner of the repo.
|
||||
if perm.IsOwner() {
|
||||
// If the poster isn't a admin, enable the owner role.
|
||||
if !poster.IsAdmin {
|
||||
roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorOwner)
|
||||
} else {
|
||||
|
||||
// Otherwise check if poster is the real repo admin.
|
||||
ok, err := access_model.IsUserRealRepoAdmin(repo, poster)
|
||||
if err != nil {
|
||||
return issues_model.RoleDescriptorNone, err
|
||||
}
|
||||
if ok {
|
||||
roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorOwner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Is the poster can write issues or pulls to the repo, enable the Writer role.
|
||||
// Only enable this if the poster doesn't have the owner role already.
|
||||
if !roleDescriptor.HasRole("Owner") && perm.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||
roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorWriter)
|
||||
return roleDescriptor, err
|
||||
}
|
||||
|
||||
// If the poster is the actual poster of the issue, enable Poster role.
|
||||
if issue.IsPoster(poster.ID) {
|
||||
roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorPoster)
|
||||
roleDescriptor.IsPoster = issue.IsPoster(poster.ID)
|
||||
|
||||
// Check if the poster is owner of the repo.
|
||||
if perm.IsOwner() {
|
||||
// If the poster isn't an admin, enable the owner role.
|
||||
if !poster.IsAdmin {
|
||||
roleDescriptor.RoleInRepo = issues_model.RoleRepoOwner
|
||||
return roleDescriptor, nil
|
||||
}
|
||||
|
||||
// Otherwise check if poster is the real repo admin.
|
||||
ok, err := access_model.IsUserRealRepoAdmin(repo, poster)
|
||||
if err != nil {
|
||||
return roleDescriptor, err
|
||||
}
|
||||
if ok {
|
||||
roleDescriptor.RoleInRepo = issues_model.RoleRepoOwner
|
||||
return roleDescriptor, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If repo is organization, check Member role
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return roleDescriptor, err
|
||||
}
|
||||
if repo.Owner.IsOrganization() {
|
||||
if isMember, err := organization.IsOrganizationMember(ctx, repo.Owner.ID, poster.ID); err != nil {
|
||||
return roleDescriptor, err
|
||||
} else if isMember {
|
||||
roleDescriptor.RoleInRepo = issues_model.RoleRepoMember
|
||||
return roleDescriptor, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If the poster is the collaborator of the repo
|
||||
if isCollaborator, err := repo_model.IsCollaborator(ctx, repo.ID, poster.ID); err != nil {
|
||||
return roleDescriptor, err
|
||||
} else if isCollaborator {
|
||||
roleDescriptor.RoleInRepo = issues_model.RoleRepoCollaborator
|
||||
return roleDescriptor, nil
|
||||
}
|
||||
|
||||
hasMergedPR, err := issues_model.HasMergedPullRequestInRepo(ctx, repo.ID, poster.ID)
|
||||
if err != nil {
|
||||
return roleDescriptor, err
|
||||
} else if hasMergedPR {
|
||||
roleDescriptor.RoleInRepo = issues_model.RoleRepoContributor
|
||||
} else {
|
||||
// only display first time contributor in the first opening pull request
|
||||
roleDescriptor.RoleInRepo = issues_model.RoleRepoFirstTimeContributor
|
||||
}
|
||||
|
||||
return roleDescriptor, nil
|
||||
|
|
|
@ -72,5 +72,5 @@ func SettingsDeleteAvatar(ctx *context.Context) {
|
|||
if err := repo_service.DeleteAvatar(ctx, ctx.Repo.Repository); err != nil {
|
||||
ctx.Flash.Error(fmt.Sprintf("DeleteAvatar: %v", err))
|
||||
}
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
|
||||
ctx.JSONRedirect(ctx.Repo.RepoLink + "/settings")
|
||||
}
|
||||
|
|
|
@ -448,21 +448,26 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||
// - Team org's owns the repository.
|
||||
// - Team has read permission to repository.
|
||||
repoOpts := &repo_model.SearchRepoOptions{
|
||||
Actor: ctx.Doer,
|
||||
OwnerID: ctx.Doer.ID,
|
||||
Private: true,
|
||||
AllPublic: false,
|
||||
AllLimited: false,
|
||||
Actor: ctx.Doer,
|
||||
OwnerID: ctx.Doer.ID,
|
||||
Private: true,
|
||||
AllPublic: false,
|
||||
AllLimited: false,
|
||||
Collaborate: util.OptionalBoolNone,
|
||||
UnitType: unitType,
|
||||
Archived: util.OptionalBoolFalse,
|
||||
}
|
||||
if team != nil {
|
||||
repoOpts.TeamID = team.ID
|
||||
}
|
||||
accessibleRepos := container.Set[int64]{}
|
||||
{
|
||||
ids, _, err := repo_model.SearchRepositoryIDs(repoOpts)
|
||||
if err != nil {
|
||||
ctx.ServerError("SearchRepositoryIDs", err)
|
||||
return
|
||||
}
|
||||
accessibleRepos.AddMultiple(ids...)
|
||||
opts.RepoIDs = ids
|
||||
if len(opts.RepoIDs) == 0 {
|
||||
// no repos found, don't let the indexer return all repos
|
||||
|
@ -489,40 +494,16 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||
keyword := strings.Trim(ctx.FormString("q"), " ")
|
||||
ctx.Data["Keyword"] = keyword
|
||||
|
||||
accessibleRepos := container.Set[int64]{}
|
||||
{
|
||||
ids, err := issues_model.GetRepoIDsForIssuesOptions(opts, ctxUser)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRepoIDsForIssuesOptions", err)
|
||||
return
|
||||
}
|
||||
for _, id := range ids {
|
||||
accessibleRepos.Add(id)
|
||||
}
|
||||
}
|
||||
|
||||
// Educated guess: Do or don't show closed issues.
|
||||
isShowClosed := ctx.FormString("state") == "closed"
|
||||
opts.IsClosed = util.OptionalBoolOf(isShowClosed)
|
||||
|
||||
// Filter repos and count issues in them. Count will be used later.
|
||||
// USING NON-FINAL STATE OF opts FOR A QUERY.
|
||||
var issueCountByRepo map[int64]int64
|
||||
{
|
||||
issueIDs, err := issueIDsFromSearch(ctx, keyword, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("issueIDsFromSearch", err)
|
||||
return
|
||||
}
|
||||
if len(issueIDs) > 0 { // else, no issues found, just leave issueCountByRepo empty
|
||||
opts.IssueIDs = issueIDs
|
||||
issueCountByRepo, err = issues_model.CountIssuesByRepo(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("CountIssuesByRepo", err)
|
||||
return
|
||||
}
|
||||
opts.IssueIDs = nil // reset, the opts will be used later
|
||||
}
|
||||
issueCountByRepo, err := issue_indexer.CountIssuesByRepo(ctx, issue_indexer.ToSearchOptions(keyword, opts))
|
||||
if err != nil {
|
||||
ctx.ServerError("CountIssuesByRepo", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure page number is at least 1. Will be posted to ctx.Data.
|
||||
|
@ -551,13 +532,13 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||
|
||||
// Parse ctx.FormString("repos") and remember matched repo IDs for later.
|
||||
// Gets set when clicking filters on the issues overview page.
|
||||
repoIDs := getRepoIDs(ctx.FormString("repos"))
|
||||
if len(repoIDs) > 0 {
|
||||
// Remove repo IDs that are not accessible to the user.
|
||||
repoIDs = util.SliceRemoveAllFunc(repoIDs, func(v int64) bool {
|
||||
return !accessibleRepos.Contains(v)
|
||||
})
|
||||
opts.RepoIDs = repoIDs
|
||||
selectedRepoIDs := getRepoIDs(ctx.FormString("repos"))
|
||||
// Remove repo IDs that are not accessible to the user.
|
||||
selectedRepoIDs = util.SliceRemoveAllFunc(selectedRepoIDs, func(v int64) bool {
|
||||
return !accessibleRepos.Contains(v)
|
||||
})
|
||||
if len(selectedRepoIDs) > 0 {
|
||||
opts.RepoIDs = selectedRepoIDs
|
||||
}
|
||||
|
||||
// ------------------------------
|
||||
|
@ -568,7 +549,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||
// USING FINAL STATE OF opts FOR A QUERY.
|
||||
var issues issues_model.IssueList
|
||||
{
|
||||
issueIDs, err := issueIDsFromSearch(ctx, keyword, opts)
|
||||
issueIDs, _, err := issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, opts))
|
||||
if err != nil {
|
||||
ctx.ServerError("issueIDsFromSearch", err)
|
||||
return
|
||||
|
@ -584,6 +565,18 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||
// Add repository pointers to Issues.
|
||||
// ----------------------------------
|
||||
|
||||
// Remove repositories that should not be shown,
|
||||
// which are repositories that have no issues and are not selected by the user.
|
||||
selectedReposMap := make(map[int64]struct{}, len(selectedRepoIDs))
|
||||
for _, repoID := range selectedRepoIDs {
|
||||
selectedReposMap[repoID] = struct{}{}
|
||||
}
|
||||
for k, v := range issueCountByRepo {
|
||||
if _, ok := selectedReposMap[k]; !ok && v == 0 {
|
||||
delete(issueCountByRepo, k)
|
||||
}
|
||||
}
|
||||
|
||||
// showReposMap maps repository IDs to their Repository pointers.
|
||||
showReposMap, err := loadRepoByIDs(ctxUser, issueCountByRepo, unitType)
|
||||
if err != nil {
|
||||
|
@ -615,44 +608,10 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||
// -------------------------------
|
||||
// Fill stats to post to ctx.Data.
|
||||
// -------------------------------
|
||||
var issueStats *issues_model.IssueStats
|
||||
{
|
||||
statsOpts := issues_model.IssuesOptions{
|
||||
RepoIDs: repoIDs,
|
||||
User: ctx.Doer,
|
||||
IsPull: util.OptionalBoolOf(isPullList),
|
||||
IsClosed: util.OptionalBoolOf(isShowClosed),
|
||||
IssueIDs: nil,
|
||||
IsArchived: util.OptionalBoolFalse,
|
||||
LabelIDs: opts.LabelIDs,
|
||||
Org: org,
|
||||
Team: team,
|
||||
RepoCond: opts.RepoCond,
|
||||
}
|
||||
|
||||
if keyword != "" {
|
||||
statsOpts.RepoIDs = opts.RepoIDs
|
||||
allIssueIDs, err := issueIDsFromSearch(ctx, keyword, &statsOpts)
|
||||
if err != nil {
|
||||
ctx.ServerError("issueIDsFromSearch", err)
|
||||
return
|
||||
}
|
||||
statsOpts.IssueIDs = allIssueIDs
|
||||
}
|
||||
|
||||
if keyword != "" && len(statsOpts.IssueIDs) == 0 {
|
||||
// So it did search with the keyword, but no issue found.
|
||||
// Just set issueStats to empty.
|
||||
issueStats = &issues_model.IssueStats{}
|
||||
} else {
|
||||
// So it did search with the keyword, and found some issues. It needs to get issueStats of these issues.
|
||||
// Or the keyword is empty, so it doesn't need issueIDs as filter, just get issueStats with statsOpts.
|
||||
issueStats, err = issues_model.GetUserIssueStats(filterMode, statsOpts)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserIssueStats", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
issueStats, err := getUserIssueStats(ctx, filterMode, issue_indexer.ToSearchOptions(keyword, opts), ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("getUserIssueStats", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Will be posted to ctx.Data.
|
||||
|
@ -722,7 +681,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||
ctx.Data["IssueStats"] = issueStats
|
||||
ctx.Data["ViewType"] = viewType
|
||||
ctx.Data["SortType"] = sortType
|
||||
ctx.Data["RepoIDs"] = opts.RepoIDs
|
||||
ctx.Data["RepoIDs"] = selectedRepoIDs
|
||||
ctx.Data["IsShowClosed"] = isShowClosed
|
||||
ctx.Data["SelectLabels"] = selectedLabels
|
||||
|
||||
|
@ -777,14 +736,6 @@ func getRepoIDs(reposQuery string) []int64 {
|
|||
return repoIDs
|
||||
}
|
||||
|
||||
func issueIDsFromSearch(ctx *context.Context, keyword string, opts *issues_model.IssuesOptions) ([]int64, error) {
|
||||
ids, _, err := issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, opts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SearchIssues: %w", err)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func loadRepoByIDs(ctxUser *user_model.User, issueCountByRepo map[int64]int64, unitType unit.Type) (map[int64]*repo_model.Repository, error) {
|
||||
totalRes := make(map[int64]*repo_model.Repository, len(issueCountByRepo))
|
||||
repoIDs := make([]int64, 0, 500)
|
||||
|
@ -913,3 +864,71 @@ func UsernameSubRoute(ctx *context.Context) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer.SearchOptions, doerID int64) (*issues_model.IssueStats, error) {
|
||||
opts = opts.Copy(func(o *issue_indexer.SearchOptions) {
|
||||
o.AssigneeID = nil
|
||||
o.PosterID = nil
|
||||
o.MentionID = nil
|
||||
o.ReviewRequestedID = nil
|
||||
o.ReviewedID = nil
|
||||
})
|
||||
|
||||
var (
|
||||
err error
|
||||
ret = &issues_model.IssueStats{}
|
||||
)
|
||||
|
||||
{
|
||||
openClosedOpts := opts.Copy()
|
||||
switch filterMode {
|
||||
case issues_model.FilterModeAll, issues_model.FilterModeYourRepositories:
|
||||
case issues_model.FilterModeAssign:
|
||||
openClosedOpts.AssigneeID = &doerID
|
||||
case issues_model.FilterModeCreate:
|
||||
openClosedOpts.PosterID = &doerID
|
||||
case issues_model.FilterModeMention:
|
||||
openClosedOpts.MentionID = &doerID
|
||||
case issues_model.FilterModeReviewRequested:
|
||||
openClosedOpts.ReviewRequestedID = &doerID
|
||||
case issues_model.FilterModeReviewed:
|
||||
openClosedOpts.ReviewedID = &doerID
|
||||
}
|
||||
openClosedOpts.IsClosed = util.OptionalBoolFalse
|
||||
ret.OpenCount, err = issue_indexer.CountIssues(ctx, openClosedOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
openClosedOpts.IsClosed = util.OptionalBoolTrue
|
||||
ret.ClosedCount, err = issue_indexer.CountIssues(ctx, openClosedOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
ret.YourRepositoriesCount, err = issue_indexer.CountIssues(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret.AssignCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AssigneeID = &doerID }))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret.CreateCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.PosterID = &doerID }))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret.MentionCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.MentionID = &doerID }))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret.ReviewRequestedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewRequestedID = &doerID }))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret.ReviewedCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.ReviewedID = &doerID }))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
|
|
@ -194,7 +194,7 @@ func DeleteAvatar(ctx *context.Context) {
|
|||
ctx.Flash.Error(err.Error())
|
||||
}
|
||||
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings")
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/user/settings")
|
||||
}
|
||||
|
||||
// Organization render all the organization of the user
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package actions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
@ -24,6 +25,7 @@ import (
|
|||
"code.gitea.io/gitea/services/convert"
|
||||
|
||||
"github.com/nektos/act/pkg/jobparser"
|
||||
"github.com/nektos/act/pkg/model"
|
||||
)
|
||||
|
||||
var methodCtxKey struct{}
|
||||
|
@ -143,15 +145,15 @@ func notify(ctx context.Context, input *notifyInput) error {
|
|||
}
|
||||
|
||||
var detectedWorkflows []*actions_module.DetectedWorkflow
|
||||
workflows, err := actions_module.DetectWorkflows(gitRepo, commit, input.Event, input.Payload)
|
||||
actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig()
|
||||
workflows, schedules, err := actions_module.DetectWorkflows(gitRepo, commit, input.Event, input.Payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("DetectWorkflows: %w", err)
|
||||
}
|
||||
|
||||
if len(workflows) == 0 {
|
||||
log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RepoPath(), commit.ID)
|
||||
} else {
|
||||
actionsConfig := input.Repo.MustGetUnit(ctx, unit_model.TypeActions).ActionsConfig()
|
||||
|
||||
for _, wf := range workflows {
|
||||
if actionsConfig.IsWorkflowDisabled(wf.EntryName) {
|
||||
log.Trace("repo %s has disable workflows %s", input.Repo.RepoPath(), wf.EntryName)
|
||||
|
@ -171,7 +173,7 @@ func notify(ctx context.Context, input *notifyInput) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("gitRepo.GetCommit: %w", err)
|
||||
}
|
||||
baseWorkflows, err := actions_module.DetectWorkflows(gitRepo, baseCommit, input.Event, input.Payload)
|
||||
baseWorkflows, _, err := actions_module.DetectWorkflows(gitRepo, baseCommit, input.Event, input.Payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("DetectWorkflows: %w", err)
|
||||
}
|
||||
|
@ -186,7 +188,22 @@ func notify(ctx context.Context, input *notifyInput) error {
|
|||
}
|
||||
}
|
||||
|
||||
if err := handleSchedules(ctx, schedules, commit, input); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return handleWorkflows(ctx, detectedWorkflows, commit, input, ref)
|
||||
}
|
||||
|
||||
func handleWorkflows(
|
||||
ctx context.Context,
|
||||
detectedWorkflows []*actions_module.DetectedWorkflow,
|
||||
commit *git.Commit,
|
||||
input *notifyInput,
|
||||
ref string,
|
||||
) error {
|
||||
if len(detectedWorkflows) == 0 {
|
||||
log.Trace("repo %s with commit %s couldn't find workflows", input.Repo.RepoPath(), commit.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -350,3 +367,86 @@ func ifNeedApproval(ctx context.Context, run *actions_model.ActionRun, repo *rep
|
|||
log.Trace("need approval because it's the first time user %d triggered actions", user.ID)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func handleSchedules(
|
||||
ctx context.Context,
|
||||
detectedWorkflows []*actions_module.DetectedWorkflow,
|
||||
commit *git.Commit,
|
||||
input *notifyInput,
|
||||
) error {
|
||||
if len(detectedWorkflows) == 0 {
|
||||
log.Trace("repo %s with commit %s couldn't find schedules", input.Repo.RepoPath(), commit.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
branch, err := commit.GetBranchName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if branch != input.Repo.DefaultBranch {
|
||||
log.Trace("commit branch is not default branch in repo")
|
||||
return nil
|
||||
}
|
||||
|
||||
rows, _, err := actions_model.FindSchedules(ctx, actions_model.FindScheduleOptions{RepoID: input.Repo.ID})
|
||||
if err != nil {
|
||||
log.Error("FindCrons: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if len(rows) > 0 {
|
||||
if err := actions_model.DeleteScheduleTaskByRepo(ctx, input.Repo.ID); err != nil {
|
||||
log.Error("DeleteCronTaskByRepo: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
p, err := json.Marshal(input.Payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("json.Marshal: %w", err)
|
||||
}
|
||||
|
||||
crons := make([]*actions_model.ActionSchedule, 0, len(detectedWorkflows))
|
||||
for _, dwf := range detectedWorkflows {
|
||||
// Check cron job condition. Only working in default branch
|
||||
workflow, err := model.ReadWorkflow(bytes.NewReader(dwf.Content))
|
||||
if err != nil {
|
||||
log.Error("ReadWorkflow: %v", err)
|
||||
continue
|
||||
}
|
||||
schedules := workflow.OnSchedule()
|
||||
if len(schedules) == 0 {
|
||||
log.Warn("no schedule event")
|
||||
continue
|
||||
}
|
||||
|
||||
run := &actions_model.ActionSchedule{
|
||||
Title: strings.SplitN(commit.CommitMessage, "\n", 2)[0],
|
||||
RepoID: input.Repo.ID,
|
||||
OwnerID: input.Repo.OwnerID,
|
||||
WorkflowID: dwf.EntryName,
|
||||
TriggerUserID: input.Doer.ID,
|
||||
Ref: input.Ref,
|
||||
CommitSHA: commit.ID.String(),
|
||||
Event: input.Event,
|
||||
EventPayload: string(p),
|
||||
Specs: schedules,
|
||||
Content: dwf.Content,
|
||||
}
|
||||
|
||||
// cancel running jobs if the event is push
|
||||
if run.Event == webhook_module.HookEventPush {
|
||||
// cancel running jobs of the same workflow
|
||||
if err := actions_model.CancelRunningJobs(
|
||||
ctx,
|
||||
run.RepoID,
|
||||
run.Ref,
|
||||
run.WorkflowID,
|
||||
); err != nil {
|
||||
log.Error("CancelRunningJobs: %v", err)
|
||||
}
|
||||
}
|
||||
crons = append(crons, run)
|
||||
}
|
||||
|
||||
return actions_model.CreateScheduleTask(ctx, crons)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||
|
||||
"github.com/nektos/act/pkg/jobparser"
|
||||
)
|
||||
|
||||
// StartScheduleTasks start the task
|
||||
func StartScheduleTasks(ctx context.Context) error {
|
||||
return startTasks(ctx)
|
||||
}
|
||||
|
||||
// startTasks retrieves specifications in pages, creates a schedule task for each specification,
|
||||
// and updates the specification's next run time and previous run time.
|
||||
// The function returns an error if there's an issue with finding or updating the specifications.
|
||||
func startTasks(ctx context.Context) error {
|
||||
// Set the page size
|
||||
pageSize := 50
|
||||
|
||||
// Retrieve specs in pages until all specs have been retrieved
|
||||
now := time.Now()
|
||||
for page := 1; ; page++ {
|
||||
// Retrieve the specs for the current page
|
||||
specs, _, err := actions_model.FindSpecs(ctx, actions_model.FindSpecOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
},
|
||||
Next: now.Unix(),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("find specs: %w", err)
|
||||
}
|
||||
|
||||
// Loop through each spec and create a schedule task for it
|
||||
for _, row := range specs {
|
||||
// cancel running jobs if the event is push
|
||||
if row.Schedule.Event == webhook_module.HookEventPush {
|
||||
// cancel running jobs of the same workflow
|
||||
if err := actions_model.CancelRunningJobs(
|
||||
ctx,
|
||||
row.RepoID,
|
||||
row.Schedule.Ref,
|
||||
row.Schedule.WorkflowID,
|
||||
); err != nil {
|
||||
log.Error("CancelRunningJobs: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := CreateScheduleTask(ctx, row.Schedule); err != nil {
|
||||
log.Error("CreateScheduleTask: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse the spec
|
||||
schedule, err := row.Parse()
|
||||
if err != nil {
|
||||
log.Error("Parse: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update the spec's next run time and previous run time
|
||||
row.Prev = row.Next
|
||||
row.Next = timeutil.TimeStamp(schedule.Next(now.Add(1 * time.Minute)).Unix())
|
||||
if err := actions_model.UpdateScheduleSpec(ctx, row, "prev", "next"); err != nil {
|
||||
log.Error("UpdateScheduleSpec: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Stop if all specs have been retrieved
|
||||
if len(specs) < pageSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateScheduleTask creates a scheduled task from a cron action schedule.
|
||||
// It creates an action run based on the schedule, inserts it into the database, and creates commit statuses for each job.
|
||||
func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) error {
|
||||
// Create a new action run based on the schedule
|
||||
run := &actions_model.ActionRun{
|
||||
Title: cron.Title,
|
||||
RepoID: cron.RepoID,
|
||||
OwnerID: cron.OwnerID,
|
||||
WorkflowID: cron.WorkflowID,
|
||||
TriggerUserID: cron.TriggerUserID,
|
||||
Ref: cron.Ref,
|
||||
CommitSHA: cron.CommitSHA,
|
||||
Event: cron.Event,
|
||||
EventPayload: cron.EventPayload,
|
||||
Status: actions_model.StatusWaiting,
|
||||
}
|
||||
|
||||
// Parse the workflow specification from the cron schedule
|
||||
workflows, err := jobparser.Parse(cron.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Insert the action run and its associated jobs into the database
|
||||
if err := actions_model.InsertRun(ctx, run, workflows); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Retrieve the jobs for the newly created action run
|
||||
jobs, _, err := actions_model.FindRunJobs(ctx, actions_model.FindRunJobOptions{RunID: run.ID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create commit statuses for each job
|
||||
for _, job := range jobs {
|
||||
if err := createCommitStatus(ctx, job); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Return nil if no errors occurred
|
||||
return nil
|
||||
}
|
|
@ -22,6 +22,7 @@ func ToAPIRelease(ctx context.Context, repo *repo_model.Repository, r *repo_mode
|
|||
HTMLURL: r.HTMLURL(),
|
||||
TarURL: r.TarURL(),
|
||||
ZipURL: r.ZipURL(),
|
||||
UploadURL: r.APIUploadURL(),
|
||||
IsDraft: r.IsDraft,
|
||||
IsPrerelease: r.IsPrerelease,
|
||||
CreatedAt: r.CreatedUnix.AsTime(),
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package convert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRelease_ToRelease(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
release1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: 1})
|
||||
release1.LoadAttributes(db.DefaultContext)
|
||||
|
||||
apiRelease := ToAPIRelease(db.DefaultContext, repo1, release1)
|
||||
assert.NotNil(t, apiRelease)
|
||||
assert.EqualValues(t, 1, apiRelease.ID)
|
||||
assert.EqualValues(t, "https://try.gitea.io/api/v1/repos/user2/repo1/releases/1", apiRelease.URL)
|
||||
assert.EqualValues(t, "https://try.gitea.io/api/v1/repos/user2/repo1/releases/1/assets", apiRelease.UploadURL)
|
||||
}
|
|
@ -18,6 +18,7 @@ func initActionsTasks() {
|
|||
registerStopZombieTasks()
|
||||
registerStopEndlessTasks()
|
||||
registerCancelAbandonedJobs()
|
||||
registerScheduleTasks()
|
||||
}
|
||||
|
||||
func registerStopZombieTasks() {
|
||||
|
@ -49,3 +50,16 @@ func registerCancelAbandonedJobs() {
|
|||
return actions_service.CancelAbandonedJobs(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
// registerScheduleTasks registers a scheduled task that runs every minute to start any due schedule tasks.
|
||||
func registerScheduleTasks() {
|
||||
// Register the task with a unique name, enabled status, and schedule for every minute.
|
||||
RegisterTaskFatal("start_schedule_tasks", &BaseConfig{
|
||||
Enabled: true,
|
||||
RunAtStart: false,
|
||||
Schedule: "@every 1m",
|
||||
}, func(ctx context.Context, _ *user_model.User, cfg Config) error {
|
||||
// Call the function to start schedule tasks and pass the context.
|
||||
return actions_service.StartScheduleTasks(ctx)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -97,23 +97,40 @@ func (f *FeishuPayload) Push(p *api.PushPayload) (api.Payloader, error) {
|
|||
|
||||
// Issue implements PayloadConvertor Issue method
|
||||
func (f *FeishuPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
|
||||
text, issueTitle, attachmentText, _ := getIssuesPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return newFeishuTextPayload(issueTitle + "\r\n" + text + "\r\n\r\n" + attachmentText), nil
|
||||
title, link, by, operator, result, assignees := getIssuesInfo(p)
|
||||
var res api.Payloader
|
||||
if assignees != "" {
|
||||
if p.Action == api.HookIssueAssigned || p.Action == api.HookIssueUnassigned || p.Action == api.HookIssueMilestoned {
|
||||
res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.Issue.Body))
|
||||
} else {
|
||||
res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.Issue.Body))
|
||||
}
|
||||
} else {
|
||||
res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.Issue.Body))
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// IssueComment implements PayloadConvertor IssueComment method
|
||||
func (f *FeishuPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
|
||||
text, issueTitle, _ := getIssueCommentPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return newFeishuTextPayload(issueTitle + "\r\n" + text + "\r\n\r\n" + p.Comment.Body), nil
|
||||
title, link, by, operator := getIssuesCommentInfo(p)
|
||||
return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.Comment.Body)), nil
|
||||
}
|
||||
|
||||
// PullRequest implements PayloadConvertor PullRequest method
|
||||
func (f *FeishuPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
|
||||
text, issueTitle, attachmentText, _ := getPullRequestPayloadInfo(p, noneLinkFormatter, true)
|
||||
|
||||
return newFeishuTextPayload(issueTitle + "\r\n" + text + "\r\n\r\n" + attachmentText), nil
|
||||
title, link, by, operator, result, assignees := getPullRequestInfo(p)
|
||||
var res api.Payloader
|
||||
if assignees != "" {
|
||||
if p.Action == api.HookIssueAssigned || p.Action == api.HookIssueUnassigned || p.Action == api.HookIssueMilestoned {
|
||||
res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.PullRequest.Body))
|
||||
} else {
|
||||
res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.PullRequest.Body))
|
||||
}
|
||||
} else {
|
||||
res = newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.PullRequest.Body))
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Review implements PayloadConvertor Review method
|
||||
|
|
|
@ -72,7 +72,7 @@ func TestFeishuPayload(t *testing.T) {
|
|||
require.NotNil(t, pl)
|
||||
require.IsType(t, &FeishuPayload{}, pl)
|
||||
|
||||
assert.Equal(t, "#2 crash\r\n[test/repo] Issue opened: #2 crash by user1\r\n\r\nissue body", pl.(*FeishuPayload).Content.Text)
|
||||
assert.Equal(t, "[Issue-test/repo #2]: opened\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.(*FeishuPayload).Content.Text)
|
||||
|
||||
p.Action = api.HookIssueClosed
|
||||
pl, err = d.Issue(p)
|
||||
|
@ -80,7 +80,7 @@ func TestFeishuPayload(t *testing.T) {
|
|||
require.NotNil(t, pl)
|
||||
require.IsType(t, &FeishuPayload{}, pl)
|
||||
|
||||
assert.Equal(t, "#2 crash\r\n[test/repo] Issue closed: #2 crash by user1", pl.(*FeishuPayload).Content.Text)
|
||||
assert.Equal(t, "[Issue-test/repo #2]: closed\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\nAssignees: user1\n\nissue body", pl.(*FeishuPayload).Content.Text)
|
||||
})
|
||||
|
||||
t.Run("IssueComment", func(t *testing.T) {
|
||||
|
@ -92,7 +92,7 @@ func TestFeishuPayload(t *testing.T) {
|
|||
require.NotNil(t, pl)
|
||||
require.IsType(t, &FeishuPayload{}, pl)
|
||||
|
||||
assert.Equal(t, "#2 crash\r\n[test/repo] New comment on issue #2 crash by user1\r\n\r\nmore info needed", pl.(*FeishuPayload).Content.Text)
|
||||
assert.Equal(t, "[Comment-test/repo #2]: created\ncrash\nhttp://localhost:3000/test/repo/issues/2\nIssue by user1\nOperator: user1\n\nmore info needed", pl.(*FeishuPayload).Content.Text)
|
||||
})
|
||||
|
||||
t.Run("PullRequest", func(t *testing.T) {
|
||||
|
@ -104,7 +104,7 @@ func TestFeishuPayload(t *testing.T) {
|
|||
require.NotNil(t, pl)
|
||||
require.IsType(t, &FeishuPayload{}, pl)
|
||||
|
||||
assert.Equal(t, "#12 Fix bug\r\n[test/repo] Pull request opened: #12 Fix bug by user1\r\n\r\nfixes bug #2", pl.(*FeishuPayload).Content.Text)
|
||||
assert.Equal(t, "[PullRequest-test/repo #12]: opened\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\nAssignees: user1\n\nfixes bug #2", pl.(*FeishuPayload).Content.Text)
|
||||
})
|
||||
|
||||
t.Run("PullRequestComment", func(t *testing.T) {
|
||||
|
@ -116,7 +116,7 @@ func TestFeishuPayload(t *testing.T) {
|
|||
require.NotNil(t, pl)
|
||||
require.IsType(t, &FeishuPayload{}, pl)
|
||||
|
||||
assert.Equal(t, "#12 Fix bug\r\n[test/repo] New comment on pull request #12 Fix bug by user1\r\n\r\nchanges requested", pl.(*FeishuPayload).Content.Text)
|
||||
assert.Equal(t, "[Comment-test/repo #12]: created\nFix bug\nhttp://localhost:3000/test/repo/pulls/12\nPullRequest by user1\nOperator: user1\n\nchanges requested", pl.(*FeishuPayload).Content.Text)
|
||||
})
|
||||
|
||||
t.Run("Review", func(t *testing.T) {
|
||||
|
|
|
@ -28,6 +28,69 @@ func htmlLinkFormatter(url, text string) string {
|
|||
return fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(url), html.EscapeString(text))
|
||||
}
|
||||
|
||||
// getPullRequestInfo gets the information for a pull request
|
||||
func getPullRequestInfo(p *api.PullRequestPayload) (title, link, by, operator, operateResult, assignees string) {
|
||||
title = fmt.Sprintf("[PullRequest-%s #%d]: %s\n%s", p.Repository.FullName, p.PullRequest.Index, p.Action, p.PullRequest.Title)
|
||||
assignList := p.PullRequest.Assignees
|
||||
assignStringList := make([]string, len(assignList))
|
||||
|
||||
for i, user := range assignList {
|
||||
assignStringList[i] = user.UserName
|
||||
}
|
||||
if p.Action == api.HookIssueAssigned {
|
||||
operateResult = fmt.Sprintf("%s assign this to %s", p.Sender.UserName, assignList[len(assignList)-1].UserName)
|
||||
} else if p.Action == api.HookIssueUnassigned {
|
||||
operateResult = fmt.Sprintf("%s unassigned this for someone", p.Sender.UserName)
|
||||
} else if p.Action == api.HookIssueMilestoned {
|
||||
operateResult = fmt.Sprintf("%s/milestone/%d", p.Repository.HTMLURL, p.PullRequest.Milestone.ID)
|
||||
}
|
||||
link = p.PullRequest.HTMLURL
|
||||
by = fmt.Sprintf("PullRequest by %s", p.PullRequest.Poster.UserName)
|
||||
if len(assignStringList) > 0 {
|
||||
assignees = fmt.Sprintf("Assignees: %s", strings.Join(assignStringList, ", "))
|
||||
}
|
||||
operator = fmt.Sprintf("Operator: %s", p.Sender.UserName)
|
||||
return title, link, by, operator, operateResult, assignees
|
||||
}
|
||||
|
||||
// getIssuesInfo gets the information for an issue
|
||||
func getIssuesInfo(p *api.IssuePayload) (issueTitle, link, by, operator, operateResult, assignees string) {
|
||||
issueTitle = fmt.Sprintf("[Issue-%s #%d]: %s\n%s", p.Repository.FullName, p.Issue.Index, p.Action, p.Issue.Title)
|
||||
assignList := p.Issue.Assignees
|
||||
assignStringList := make([]string, len(assignList))
|
||||
|
||||
for i, user := range assignList {
|
||||
assignStringList[i] = user.UserName
|
||||
}
|
||||
if p.Action == api.HookIssueAssigned {
|
||||
operateResult = fmt.Sprintf("%s assign this to %s", p.Sender.UserName, assignList[len(assignList)-1].UserName)
|
||||
} else if p.Action == api.HookIssueUnassigned {
|
||||
operateResult = fmt.Sprintf("%s unassigned this for someone", p.Sender.UserName)
|
||||
} else if p.Action == api.HookIssueMilestoned {
|
||||
operateResult = fmt.Sprintf("%s/milestone/%d", p.Repository.HTMLURL, p.Issue.Milestone.ID)
|
||||
}
|
||||
link = p.Issue.HTMLURL
|
||||
by = fmt.Sprintf("Issue by %s", p.Issue.Poster.UserName)
|
||||
if len(assignStringList) > 0 {
|
||||
assignees = fmt.Sprintf("Assignees: %s", strings.Join(assignStringList, ", "))
|
||||
}
|
||||
operator = fmt.Sprintf("Operator: %s", p.Sender.UserName)
|
||||
return issueTitle, link, by, operator, operateResult, assignees
|
||||
}
|
||||
|
||||
// getIssuesCommentInfo gets the information for a comment
|
||||
func getIssuesCommentInfo(p *api.IssueCommentPayload) (title, link, by, operator string) {
|
||||
title = fmt.Sprintf("[Comment-%s #%d]: %s\n%s", p.Repository.FullName, p.Issue.Index, p.Action, p.Issue.Title)
|
||||
link = p.Issue.HTMLURL
|
||||
if p.IsPull {
|
||||
by = fmt.Sprintf("PullRequest by %s", p.Issue.Poster.UserName)
|
||||
} else {
|
||||
by = fmt.Sprintf("Issue by %s", p.Issue.Poster.UserName)
|
||||
}
|
||||
operator = fmt.Sprintf("Operator: %s", p.Sender.UserName)
|
||||
return title, link, by, operator
|
||||
}
|
||||
|
||||
func getIssuesPayloadInfo(p *api.IssuePayload, linkFormatter linkFormatter, withSender bool) (string, string, string, int) {
|
||||
repoLink := linkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
|
||||
issueTitle := fmt.Sprintf("#%d %s", p.Index, p.Issue.Title)
|
||||
|
|
|
@ -123,6 +123,10 @@ func issueTestPayload() *api.IssuePayload {
|
|||
HTMLURL: "http://localhost:3000/test/repo/issues/2",
|
||||
Title: "crash",
|
||||
Body: "issue body",
|
||||
Poster: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
Assignees: []*api.User{
|
||||
{
|
||||
UserName: "user1",
|
||||
|
@ -161,7 +165,11 @@ func issueCommentTestPayload() *api.IssueCommentPayload {
|
|||
URL: "http://localhost:3000/api/v1/repos/test/repo/issues/2",
|
||||
HTMLURL: "http://localhost:3000/test/repo/issues/2",
|
||||
Title: "crash",
|
||||
Body: "this happened",
|
||||
Poster: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
Body: "this happened",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -190,6 +198,10 @@ func pullRequestCommentTestPayload() *api.IssueCommentPayload {
|
|||
HTMLURL: "http://localhost:3000/test/repo/pulls/12",
|
||||
Title: "Fix bug",
|
||||
Body: "fixes bug #2",
|
||||
Poster: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
},
|
||||
IsPull: true,
|
||||
}
|
||||
|
@ -254,6 +266,10 @@ func pullRequestTestPayload() *api.PullRequestPayload {
|
|||
Title: "Fix bug",
|
||||
Body: "fixes bug #2",
|
||||
Mergeable: true,
|
||||
Poster: &api.User{
|
||||
UserName: "user1",
|
||||
AvatarURL: "http://localhost:3000/user1/avatar",
|
||||
},
|
||||
Assignees: []*api.User{
|
||||
{
|
||||
UserName: "user1",
|
||||
|
|
|
@ -186,7 +186,7 @@
|
|||
|
||||
<div class="field">
|
||||
<button class="ui green button">{{$.locale.Tr "settings.update_avatar"}}</button>
|
||||
<button class="ui red button link-action" data-url="{{.Link}}/avatar/delete" data-redirect="{{.Link}}">{{$.locale.Tr "settings.delete_current_avatar"}}</button>
|
||||
<button class="ui red button link-action" data-url="{{.Link}}/avatar/delete">{{$.locale.Tr "settings.delete_current_avatar"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -48,6 +48,7 @@
|
|||
</div>
|
||||
<div class="flex-item-body">
|
||||
Very loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong content
|
||||
<span class="text truncate">Truncate very loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong content</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-item-trailing">
|
||||
|
|
|
@ -94,7 +94,7 @@
|
|||
|
||||
<div class="field">
|
||||
<button class="ui green button">{{$.locale.Tr "settings.update_avatar"}}</button>
|
||||
<button class="ui red button link-action" data-url="{{.Link}}/avatar/delete" data-redirect="{{.Link}}">{{$.locale.Tr "settings.delete_current_avatar"}}</button>
|
||||
<button class="ui red button link-action" data-url="{{.Link}}/avatar/delete">{{$.locale.Tr "settings.delete_current_avatar"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
|
||||
<div class="js-branch-tag-selector {{if .ContainerClasses}}{{.ContainerClasses}}{{end}}">
|
||||
{{/* show dummy elements before Vue componment is mounted, this code must match the code in BranchTagSelector.vue */}}
|
||||
<div class="ui floating filter dropdown custom">
|
||||
<div class="ui dropdown custom">
|
||||
<button class="branch-dropdown-button gt-ellipsis ui basic small compact button gt-df gt-m-0">
|
||||
<span class="text gt-df gt-ac gt-mr-2">
|
||||
{{if .release}}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<h4 class="ui top attached header commits-table gt-df gt-ac gt-sb">
|
||||
<div class="commits-table-left gt-df gt-ac">
|
||||
{{if or .PageIsCommits (gt .CommitCount 0)}}
|
||||
{{.CommitCount}} {{.locale.Tr "repo.commits.commits"}} {{if .RefName}}({{.RefName}}){{end}}
|
||||
{{.CommitCount}} {{.locale.Tr "repo.commits.commits"}}
|
||||
{{else if .IsNothingToCompare}}
|
||||
{{.locale.Tr "repo.commits.nothing_to_compare"}} {{if .RefName}}({{.RefName}}){{end}}
|
||||
{{.locale.Tr "repo.commits.nothing_to_compare"}}
|
||||
{{else}}
|
||||
{{.locale.Tr "repo.commits.no_commits" $.BaseBranch $.HeadBranch}} {{if .RefName}}({{.RefName}}){{end}}
|
||||
{{.locale.Tr "repo.commits.no_commits" $.BaseBranch $.HeadBranch}}
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="commits-table-right gt-whitespace-nowrap">
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="diff-detail-actions gt-df gt-ac gt-gap-2 gt-fw">
|
||||
<div class="diff-detail-actions">
|
||||
{{if and .PageIsPullFiles $.SignedUserID (not .IsArchived) (not .DiffNotAvailable)}}
|
||||
<div class="gt-df gt-ac gt-fc gt-whitespace-nowrap gt-mr-2">
|
||||
<label for="viewed-files-summary" id="viewed-files-summary-label" data-text-changed-template="{{.locale.Tr "repo.pulls.viewed_files_label"}}">
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="gt-hidden">
|
||||
<div class="image-diff-tabs is-loading">
|
||||
<div class="ui bottom attached tab image-diff-container active" data-tab="diff-side-by-side-{{.file.Index}}">
|
||||
<div class="diff-side-by-side">
|
||||
{{if .blobBase}}
|
||||
|
@ -63,10 +63,8 @@
|
|||
</div>
|
||||
<div class="ui bottom attached tab image-diff-container" data-tab="diff-overlay-{{.file.Index}}">
|
||||
<div class="diff-overlay">
|
||||
<input type="range" min="0" max="100" value="50">
|
||||
<div class="overlay-frame">
|
||||
<div class="ui centered">
|
||||
<input type="range" min="0" max="100" value="50">
|
||||
</div>
|
||||
<span class="before-container"><img class="image-before"></span>
|
||||
<span class="after-container"><img class="image-after"></span>
|
||||
</div>
|
||||
|
@ -74,7 +72,6 @@
|
|||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="ui active centered inline loader gt-mb-4"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
<div class="field">
|
||||
{{template "repo/issue/fields/header" .}}
|
||||
{{$field := .}}
|
||||
{{range $i, $opt := .item.Attributes.options}}
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="form-field-{{$field.ID}}-{{$i}}" {{if $opt.required}}readonly checked{{end}}>
|
||||
<input type="checkbox" name="form-field-{{$.item.ID}}-{{$i}}" {{if $opt.required}}readonly checked{{end}}>
|
||||
<label>{{$opt.label}}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
{{if and (.ShowRole.HasRole "Poster") (not .IgnorePoster)}}
|
||||
<div class="ui basic label role-label">
|
||||
{{ctx.Locale.Tr "repo.issues.poster"}}
|
||||
{{if and .ShowRole.IsPoster (not .IgnorePoster)}}
|
||||
<div class="ui basic label role-label" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.author_helper"}}">
|
||||
{{ctx.Locale.Tr "repo.issues.author"}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if (.ShowRole.HasRole "Writer")}}
|
||||
<div class="ui basic label role-label">
|
||||
{{ctx.Locale.Tr "repo.issues.collaborator"}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if (.ShowRole.HasRole "Owner")}}
|
||||
<div class="ui basic label role-label">
|
||||
{{ctx.Locale.Tr "repo.issues.owner"}}
|
||||
{{if .ShowRole.RoleInRepo}}
|
||||
<div class="ui basic label role-label" data-tooltip-content="{{.ShowRole.RoleInRepo.LocaleHelper ctx.Locale}}">
|
||||
{{.ShowRole.RoleInRepo.LocaleString ctx.Locale}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
</div>
|
||||
<div class="field">
|
||||
<button class="ui green button">{{$.locale.Tr "settings.update_avatar"}}</button>
|
||||
<button class="ui red button link-action" data-url="{{.Link}}/avatar/delete" data-redirect="{{.Link}}">{{$.locale.Tr "settings.delete_current_avatar"}}</button>
|
||||
<button class="ui red button link-action" data-url="{{.Link}}/avatar/delete">{{$.locale.Tr "settings.delete_current_avatar"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -1631,6 +1631,89 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/orgs/{org}/actions/secrets/{secretname}": {
|
||||
"put": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"organization"
|
||||
],
|
||||
"summary": "Update a secret value in an organization",
|
||||
"operationId": "updateOrgSecret",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of organization",
|
||||
"name": "org",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the secret",
|
||||
"name": "secretname",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/UpdateSecretOption"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "update one secret of the organization"
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#/responses/forbidden"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"organization"
|
||||
],
|
||||
"summary": "Delete a secret in an organization",
|
||||
"operationId": "deleteOrgSecret",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of organization",
|
||||
"name": "org",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the secret",
|
||||
"name": "secretname",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "delete one secret of the organization"
|
||||
},
|
||||
"403": {
|
||||
"$ref": "#/responses/forbidden"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/orgs/{org}/activities/feeds": {
|
||||
"get": {
|
||||
"produces": [
|
||||
|
@ -21007,6 +21090,10 @@
|
|||
"type": "string",
|
||||
"x-go-name": "Target"
|
||||
},
|
||||
"upload_url": {
|
||||
"type": "string",
|
||||
"x-go-name": "UploadURL"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"x-go-name": "URL"
|
||||
|
@ -21891,6 +21978,21 @@
|
|||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"UpdateSecretOption": {
|
||||
"description": "UpdateSecretOption options when updating secret",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"description": "Data of the secret to update",
|
||||
"type": "string",
|
||||
"x-go-name": "Data"
|
||||
}
|
||||
},
|
||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||
},
|
||||
"UpdateUserAvatarOption": {
|
||||
"description": "UpdateUserAvatarUserOption options when updating the user avatar",
|
||||
"type": "object",
|
||||
|
@ -23207,7 +23309,7 @@
|
|||
"parameterBodies": {
|
||||
"description": "parameterBodies",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/CreateSecretOption"
|
||||
"$ref": "#/definitions/UpdateSecretOption"
|
||||
}
|
||||
},
|
||||
"redirect": {
|
||||
|
|
|
@ -80,25 +80,21 @@
|
|||
{{end}}
|
||||
</div>
|
||||
{{if or (eq .GetOpType 5) (eq .GetOpType 18)}}
|
||||
<div class="gt-pl-5">
|
||||
{{$push := ActionContent2Commits .}}
|
||||
{{$repoLink := .GetRepoLink}}
|
||||
{{range $push.Commits}}
|
||||
{{$commitLink := printf "%s/commit/%s" $repoLink .Sha1}}
|
||||
<div class="flex-item">
|
||||
<img class="ui avatar" src="{{$push.AvatarLink $.Context .AuthorEmail}}" title="{{.AuthorName}}" width="16" height="16">
|
||||
<a class="commit-id" href="{{$commitLink}}">{{ShortSha .Sha1}}</a>
|
||||
<span class="text truncate light grey">
|
||||
{{RenderCommitMessage $.Context .Message $repoLink $.ComposeMetas}}
|
||||
</span>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if and (gt $push.Len 1) $push.CompareURL}}
|
||||
<div class="flex-item">
|
||||
<a href="{{AppSubUrl}}/{{$push.CompareURL}}">{{$.locale.Tr "action.compare_commits" $push.Len}} Β»</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{$push := ActionContent2Commits .}}
|
||||
{{$repoLink := .GetRepoLink}}
|
||||
{{range $push.Commits}}
|
||||
{{$commitLink := printf "%s/commit/%s" $repoLink .Sha1}}
|
||||
<div class="flex-text-block">
|
||||
<img class="ui avatar" src="{{$push.AvatarLink $.Context .AuthorEmail}}" title="{{.AuthorName}}" width="16" height="16">
|
||||
<a class="gt-mono" href="{{$commitLink}}">{{ShortSha .Sha1}}</a>
|
||||
<span class="text truncate light grey">
|
||||
{{RenderCommitMessage $.Context .Message $repoLink $.ComposeMetas}}
|
||||
</span>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if and (gt $push.Len 1) $push.CompareURL}}
|
||||
<a href="{{AppSubUrl}}/{{$push.CompareURL}}">{{$.locale.Tr "action.compare_commits" $push.Len}} Β»</a>
|
||||
{{end}}
|
||||
{{else if eq .GetOpType 6}}
|
||||
<span class="text truncate issue title">{{index .GetIssueInfos 1 | RenderEmoji $.Context | RenderCodeBlock}}</span>
|
||||
{{else if eq .GetOpType 7}}
|
||||
|
|
|
@ -5,29 +5,29 @@
|
|||
<div class="ui stackable grid">
|
||||
<div class="four wide column">
|
||||
<div class="ui secondary vertical filter menu gt-bg-transparent">
|
||||
<a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="{{.Link}}?type=your_repositories&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
|
||||
<a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="{{.Link}}?type=your_repositories&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
|
||||
{{.locale.Tr "home.issues.in_your_repos"}}
|
||||
<strong class="ui right">{{CountFmt .IssueStats.YourRepositoriesCount}}</strong>
|
||||
</a>
|
||||
<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{.Link}}?type=assigned&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
|
||||
<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{.Link}}?type=assigned&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
|
||||
{{.locale.Tr "repo.issues.filter_type.assigned_to_you"}}
|
||||
<strong class="ui right">{{CountFmt .IssueStats.AssignCount}}</strong>
|
||||
</a>
|
||||
<a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{.Link}}?type=created_by&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
|
||||
<a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{.Link}}?type=created_by&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
|
||||
{{.locale.Tr "repo.issues.filter_type.created_by_you"}}
|
||||
<strong class="ui right">{{CountFmt .IssueStats.CreateCount}}</strong>
|
||||
</a>
|
||||
{{if .PageIsPulls}}
|
||||
<a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="{{.Link}}?type=review_requested&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
|
||||
<a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="{{.Link}}?type=review_requested&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
|
||||
{{.locale.Tr "repo.issues.filter_type.review_requested"}}
|
||||
<strong class="ui right">{{CountFmt .IssueStats.ReviewRequestedCount}}</strong>
|
||||
</a>
|
||||
<a class="{{if eq .ViewType "reviewed_by"}}active{{end}} item" href="{{.Link}}?type=reviewed_by&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
|
||||
<a class="{{if eq .ViewType "reviewed_by"}}active{{end}} item" href="{{.Link}}?type=reviewed_by&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
|
||||
{{.locale.Tr "repo.issues.filter_type.reviewed_by_you"}}
|
||||
<strong class="ui right">{{CountFmt .IssueStats.ReviewedCount}}</strong>
|
||||
</a>
|
||||
{{end}}
|
||||
<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{.Link}}?type=mentioned&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}">
|
||||
<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{.Link}}?type=mentioned&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state={{.State}}&q={{$.Keyword}}">
|
||||
{{.locale.Tr "repo.issues.filter_type.mentioning_you"}}
|
||||
<strong class="ui right">{{CountFmt .IssueStats.MentionCount}}</strong>
|
||||
</a>
|
||||
|
|
|
@ -126,7 +126,7 @@
|
|||
|
||||
<div class="field">
|
||||
<button class="ui green button">{{$.locale.Tr "settings.update_avatar"}}</button>
|
||||
<button class="ui red button link-action" data-url="{{.Link}}/avatar/delete" data-redirect="{{.Link}}">{{$.locale.Tr "settings.delete_current_avatar"}}</button>
|
||||
<button class="ui red button link-action" data-url="{{.Link}}/avatar/delete">{{$.locale.Tr "settings.delete_current_avatar"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -31,7 +31,7 @@ func testAPIGetBranch(t *testing.T, branchName string, exists bool) {
|
|||
assert.True(t, branch.UserCanMerge)
|
||||
}
|
||||
|
||||
func testAPIGetBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) {
|
||||
func testAPIGetBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) *api.BranchProtection {
|
||||
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository)
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/branch_protections/%s?token=%s", branchName, token)
|
||||
resp := MakeRequest(t, req, expectedHTTPStatus)
|
||||
|
@ -40,7 +40,9 @@ func testAPIGetBranchProtection(t *testing.T, branchName string, expectedHTTPSta
|
|||
var branchProtection api.BranchProtection
|
||||
DecodeJSON(t, resp, &branchProtection)
|
||||
assert.EqualValues(t, branchName, branchProtection.RuleName)
|
||||
return &branchProtection
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func testAPICreateBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) {
|
||||
|
@ -186,6 +188,24 @@ func TestAPIBranchProtection(t *testing.T) {
|
|||
EnablePush: true,
|
||||
}, http.StatusOK)
|
||||
|
||||
// enable status checks, require the "test1" check to pass
|
||||
testAPIEditBranchProtection(t, "master", &api.BranchProtection{
|
||||
EnableStatusCheck: true,
|
||||
StatusCheckContexts: []string{"test1"},
|
||||
}, http.StatusOK)
|
||||
bp := testAPIGetBranchProtection(t, "master", http.StatusOK)
|
||||
assert.Equal(t, true, bp.EnableStatusCheck)
|
||||
assert.Equal(t, []string{"test1"}, bp.StatusCheckContexts)
|
||||
|
||||
// disable status checks, clear the list of required checks
|
||||
testAPIEditBranchProtection(t, "master", &api.BranchProtection{
|
||||
EnableStatusCheck: false,
|
||||
StatusCheckContexts: []string{},
|
||||
}, http.StatusOK)
|
||||
bp = testAPIGetBranchProtection(t, "master", http.StatusOK)
|
||||
assert.Equal(t, false, bp.EnableStatusCheck)
|
||||
assert.Equal(t, []string{}, bp.StatusCheckContexts)
|
||||
|
||||
testAPIDeleteBranchProtection(t, "master", http.StatusNoContent)
|
||||
|
||||
// Test branch deletion
|
||||
|
|
|
@ -4,9 +4,13 @@
|
|||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
|
@ -38,12 +42,15 @@ func TestAPIListReleases(t *testing.T) {
|
|||
case 1:
|
||||
assert.False(t, release.IsDraft)
|
||||
assert.False(t, release.IsPrerelease)
|
||||
assert.True(t, strings.HasSuffix(release.UploadURL, "/api/v1/repos/user2/repo1/releases/1/assets"), release.UploadURL)
|
||||
case 4:
|
||||
assert.True(t, release.IsDraft)
|
||||
assert.False(t, release.IsPrerelease)
|
||||
assert.True(t, strings.HasSuffix(release.UploadURL, "/api/v1/repos/user2/repo1/releases/4/assets"), release.UploadURL)
|
||||
case 5:
|
||||
assert.False(t, release.IsDraft)
|
||||
assert.True(t, release.IsPrerelease)
|
||||
assert.True(t, strings.HasSuffix(release.UploadURL, "/api/v1/repos/user2/repo1/releases/5/assets"), release.UploadURL)
|
||||
default:
|
||||
assert.NoError(t, fmt.Errorf("unexpected release: %v", release))
|
||||
}
|
||||
|
@ -248,3 +255,36 @@ func TestAPIDeleteReleaseByTagName(t *testing.T) {
|
|||
req = NewRequestf(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/tags/release-tag?token=%s", owner.Name, repo.Name, token))
|
||||
_ = MakeRequest(t, req, http.StatusNoContent)
|
||||
}
|
||||
|
||||
func TestAPIUploadAssetRelease(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
session := loginUser(t, owner.LowerName)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
r := createNewReleaseUsingAPI(t, session, token, owner, repo, "release-tag", "", "Release Tag", "test")
|
||||
|
||||
filename := "image.png"
|
||||
buff := generateImg()
|
||||
body := &bytes.Buffer{}
|
||||
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("attachment", filename)
|
||||
assert.NoError(t, err)
|
||||
_, err = io.Copy(part, &buff)
|
||||
assert.NoError(t, err)
|
||||
err = writer.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := NewRequestWithBody(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets?name=test-asset&token=%s", owner.Name, repo.Name, r.ID, token), body)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
var attachment *api.Attachment
|
||||
DecodeJSON(t, resp, &attachment)
|
||||
|
||||
assert.EqualValues(t, "test-asset", attachment.Name)
|
||||
assert.EqualValues(t, 104, attachment.Size)
|
||||
}
|
||||
|
|
|
@ -96,10 +96,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.feeds .commit-id {
|
||||
font-family: var(--fonts-monospace);
|
||||
}
|
||||
|
||||
.feeds code {
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
.image-diff-tabs {
|
||||
min-height: 60px;
|
||||
|
||||
}
|
||||
.image-diff-tabs.is-loading .tab {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.image-diff-container {
|
||||
text-align: center;
|
||||
padding: 1em 0;
|
||||
padding: 0.5em 0 1em;
|
||||
}
|
||||
|
||||
.image-diff-container img {
|
||||
|
@ -31,6 +39,7 @@
|
|||
|
||||
.image-diff-container .diff-swipe {
|
||||
margin: auto;
|
||||
padding: 1em 0;
|
||||
}
|
||||
|
||||
.image-diff-container .diff-swipe .swipe-frame {
|
||||
|
@ -89,7 +98,7 @@
|
|||
}
|
||||
|
||||
.image-diff-container .diff-overlay .overlay-frame {
|
||||
margin: 0 auto;
|
||||
margin: 1em auto 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
|
|
@ -57,13 +57,6 @@ form.single-button-form.is-loading .button {
|
|||
background: transparent;
|
||||
}
|
||||
|
||||
/* TODO: not needed, use "is-loading small-loading-icon" instead */
|
||||
.btn-octicon.is-loading::after {
|
||||
border-width: 2px;
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
||||
/* TODO: not needed, use "is-loading small-loading-icon" instead */
|
||||
code.language-math.is-loading::after {
|
||||
padding: 0;
|
||||
|
|
|
@ -1625,10 +1625,11 @@
|
|||
}
|
||||
|
||||
.diff-detail-actions {
|
||||
/* prevent font-size from increasing element height so that .diff-detail-box comes
|
||||
out with height of 47px (one line) and 77px (two lines), which is important for
|
||||
position: sticky */
|
||||
height: 33px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
flex-wrap: wrap;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.diff-detail-actions > *,
|
||||
|
@ -1642,12 +1643,7 @@
|
|||
padding-top: 0.25rem;
|
||||
}
|
||||
.repository .diff-detail-box .diff-detail-actions .ui.button:not(.btn-submit) {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,8 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
flex-basis: 60%;
|
||||
flex-basis: 60%; /* avoid wrapping the "flex-item-trailing" too aggressively */
|
||||
min-width: 0; /* make the "text truncate" work, otherwise the flex axis is not limited and the text just overflows */
|
||||
}
|
||||
|
||||
.flex-item-header {
|
||||
|
@ -66,6 +67,7 @@
|
|||
font-size: 16px;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
word-break: break-word;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.flex-item .flex-item-title a {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="ui floating filter dropdown custom">
|
||||
<div class="ui dropdown custom">
|
||||
<button class="branch-dropdown-button gt-ellipsis ui basic small compact button gt-df gt-m-0" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible">
|
||||
<span class="text gt-df gt-ac gt-mr-2">
|
||||
<template v-if="release">{{ textReleaseCompare }}</template>
|
||||
|
|
|
@ -95,14 +95,14 @@ async function fetchActionDoRequest(actionElem, url, opt) {
|
|||
const data = await resp.json();
|
||||
// the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
|
||||
// but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
|
||||
await showErrorToast(data.errorMessage || `server error: ${resp.status}`);
|
||||
showErrorToast(data.errorMessage || `server error: ${resp.status}`);
|
||||
} else {
|
||||
await showErrorToast(`server error: ${resp.status}`);
|
||||
showErrorToast(`server error: ${resp.status}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('error when doRequest', e);
|
||||
actionElem.classList.remove('is-loading', 'small-loading-icon');
|
||||
await showErrorToast(i18n.network_error);
|
||||
showErrorToast(i18n.network_error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ export function initCopyContent() {
|
|||
// the text to copy is not in the DOM or it is an image which should be
|
||||
// fetched to copy in full resolution
|
||||
if (link) {
|
||||
btn.classList.add('is-loading');
|
||||
btn.classList.add('is-loading', 'small-loading-icon');
|
||||
try {
|
||||
const res = await fetch(link, {credentials: 'include', redirect: 'follow'});
|
||||
const contentType = res.headers.get('content-type');
|
||||
|
@ -32,7 +32,7 @@ export function initCopyContent() {
|
|||
} catch {
|
||||
return showTemporaryTooltip(btn, i18n.copy_error);
|
||||
} finally {
|
||||
btn.classList.remove('is-loading');
|
||||
btn.classList.remove('is-loading', 'small-loading-icon');
|
||||
}
|
||||
} else { // text, read from DOM
|
||||
const lineEls = document.querySelectorAll('.file-view .lines-code');
|
||||
|
|
|
@ -130,8 +130,7 @@ export function initImageDiff() {
|
|||
initOverlay(createContext($imageAfter[2], $imageBefore[2]));
|
||||
}
|
||||
|
||||
$container.find('> .gt-hidden').removeClass('gt-hidden');
|
||||
hideElem($container.find('.ui.loader'));
|
||||
$container.find('> .image-diff-tabs').removeClass('is-loading');
|
||||
}
|
||||
|
||||
function initSideBySide(sizes) {
|
||||
|
@ -205,7 +204,7 @@ export function initImageDiff() {
|
|||
});
|
||||
$container.find('.diff-swipe').css({
|
||||
width: sizes.max.width * factor + 2,
|
||||
height: sizes.max.height * factor + 4
|
||||
height: sizes.max.height * factor + 30 /* extra height for inner "position: absolute" elements */,
|
||||
});
|
||||
$container.find('.swipe-bar').on('mousedown', function(e) {
|
||||
e.preventDefault();
|
||||
|
@ -261,7 +260,7 @@ export function initImageDiff() {
|
|||
// the "css(width, height)" is somewhat hacky and not easy to understand, it could be improved in the future
|
||||
sizes.image2.parent().parent().css({
|
||||
width: sizes.max.width * factor + 2,
|
||||
height: sizes.max.height * factor + 2 + 20 /* extra height for inner "position: absolute" elements */,
|
||||
height: sizes.max.height * factor + 2,
|
||||
});
|
||||
|
||||
const $range = $container.find("input[type='range']");
|
||||
|
|
|
@ -54,9 +54,10 @@ export function initRepoCommentForm() {
|
|||
}
|
||||
|
||||
if ($commentForm.find('.field.combo-editor-dropzone').length) {
|
||||
// at the moment, if a form has multiple combo-markdown-editors, it must be a issue template form
|
||||
// at the moment, if a form has multiple combo-markdown-editors, it must be an issue template form
|
||||
initIssueTemplateCommentEditors($commentForm);
|
||||
} else {
|
||||
} else if ($commentForm.find('.combo-markdown-editor').length) {
|
||||
// it's quite unclear about the "comment form" elements, sometimes it's for issue comment, sometimes it's for file editor/uploader message
|
||||
initSingleCommentEditor($commentForm);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {htmlEscape} from 'escape-goat';
|
||||
import {svg} from '../svg.js';
|
||||
import Toastify from 'toastify-js';
|
||||
import Toastify from 'toastify-js'; // don't use "async import", because when network error occurs, the "async import" also fails and nothing is shown
|
||||
|
||||
const levels = {
|
||||
info: {
|
||||
|
@ -21,9 +21,7 @@ const levels = {
|
|||
};
|
||||
|
||||
// See https://github.com/apvarun/toastify-js#api for options
|
||||
async function showToast(message, level, {gravity, position, duration, ...other} = {}) {
|
||||
if (!message) return;
|
||||
|
||||
function showToast(message, level, {gravity, position, duration, ...other} = {}) {
|
||||
const {icon, background, duration: levelDuration} = levels[level ?? 'info'];
|
||||
|
||||
const toast = Toastify({
|
||||
|
@ -41,20 +39,17 @@ async function showToast(message, level, {gravity, position, duration, ...other}
|
|||
});
|
||||
|
||||
toast.showToast();
|
||||
|
||||
toast.toastElement.querySelector('.toast-close').addEventListener('click', () => {
|
||||
toast.removeElement(toast.toastElement);
|
||||
});
|
||||
toast.toastElement.querySelector('.toast-close').addEventListener('click', () => toast.hideToast());
|
||||
}
|
||||
|
||||
export async function showInfoToast(message, opts) {
|
||||
return await showToast(message, 'info', opts);
|
||||
export function showInfoToast(message, opts) {
|
||||
return showToast(message, 'info', opts);
|
||||
}
|
||||
|
||||
export async function showWarningToast(message, opts) {
|
||||
return await showToast(message, 'warning', opts);
|
||||
export function showWarningToast(message, opts) {
|
||||
return showToast(message, 'warning', opts);
|
||||
}
|
||||
|
||||
export async function showErrorToast(message, opts) {
|
||||
return await showToast(message, 'error', opts);
|
||||
export function showErrorToast(message, opts) {
|
||||
return showToast(message, 'error', opts);
|
||||
}
|
||||
|
|
|
@ -2,16 +2,16 @@ import {test, expect} from 'vitest';
|
|||
import {showInfoToast, showErrorToast, showWarningToast} from './toast.js';
|
||||
|
||||
test('showInfoToast', async () => {
|
||||
await showInfoToast('success π', {duration: -1});
|
||||
showInfoToast('success π', {duration: -1});
|
||||
expect(document.querySelector('.toastify')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('showWarningToast', async () => {
|
||||
await showWarningToast('warning π', {duration: -1});
|
||||
showWarningToast('warning π', {duration: -1});
|
||||
expect(document.querySelector('.toastify')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('showErrorToast', async () => {
|
||||
await showErrorToast('error π', {duration: -1});
|
||||
showErrorToast('error π', {duration: -1});
|
||||
expect(document.querySelector('.toastify')).toBeTruthy();
|
||||
});
|
||||
|
|
Loadingβ¦
Reference in New Issue