This commit is contained in:
@@ -12,23 +12,23 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/updatechecker"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/cron"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
release_service "code.gitea.io/gitea/services/release"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
activities_model "gitea.dev/models/activities"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/base"
|
||||
"gitea.dev/modules/cache"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/httplib"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/updatechecker"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/cron"
|
||||
"gitea.dev/services/forms"
|
||||
release_service "gitea.dev/services/release"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -7,73 +7,15 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/services/contexttest"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/services/contexttest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestShadowPassword(t *testing.T) {
|
||||
kases := []struct {
|
||||
Provider string
|
||||
CfgItem string
|
||||
Result string
|
||||
}{
|
||||
{
|
||||
Provider: "redis",
|
||||
CfgItem: "network=tcp,addr=:6379,password=gitea,db=0,pool_size=100,idle_timeout=180",
|
||||
Result: "network=tcp,addr=:6379,password=******,db=0,pool_size=100,idle_timeout=180",
|
||||
},
|
||||
{
|
||||
Provider: "mysql",
|
||||
CfgItem: "root:@tcp(localhost:3306)/gitea?charset=utf8",
|
||||
Result: "root:******@tcp(localhost:3306)/gitea?charset=utf8",
|
||||
},
|
||||
{
|
||||
Provider: "mysql",
|
||||
CfgItem: "/gitea?charset=utf8",
|
||||
Result: "/gitea?charset=utf8",
|
||||
},
|
||||
{
|
||||
Provider: "mysql",
|
||||
CfgItem: "user:mypassword@/dbname",
|
||||
Result: "user:******@/dbname",
|
||||
},
|
||||
{
|
||||
Provider: "postgres",
|
||||
CfgItem: "user=pqgotest dbname=pqgotest sslmode=verify-full",
|
||||
Result: "user=pqgotest dbname=pqgotest sslmode=verify-full",
|
||||
},
|
||||
{
|
||||
Provider: "postgres",
|
||||
CfgItem: "user=pqgotest password= dbname=pqgotest sslmode=verify-full",
|
||||
Result: "user=pqgotest password=****** dbname=pqgotest sslmode=verify-full",
|
||||
},
|
||||
{
|
||||
Provider: "postgres",
|
||||
CfgItem: "postgres://user:pass@hostname/dbname",
|
||||
Result: "postgres://user:******@hostname/dbname",
|
||||
},
|
||||
{
|
||||
Provider: "couchbase",
|
||||
CfgItem: "http://dev-couchbase.example.com:8091/",
|
||||
Result: "http://dev-couchbase.example.com:8091/",
|
||||
},
|
||||
{
|
||||
Provider: "couchbase",
|
||||
CfgItem: "http://user:the_password@dev-couchbase.example.com:8091/",
|
||||
Result: "http://user:******@dev-couchbase.example.com:8091/",
|
||||
},
|
||||
}
|
||||
|
||||
for _, k := range kases {
|
||||
assert.Equal(t, k.Result, shadowPassword(k.Provider, k.CfgItem))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelfCheckPost(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.PublicURLDetection)()
|
||||
defer test.MockVariableValue(&setting.AppURL, "http://config/sub/")()
|
||||
|
||||
@@ -6,12 +6,12 @@ package admin
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
user_setting "code.gitea.io/gitea/routers/web/user/setting"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
user_setting "gitea.dev/routers/web/user/setting"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -12,22 +12,22 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/auth/pam"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
auth_service "code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/auth/source/ldap"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
pam_service "code.gitea.io/gitea/services/auth/source/pam"
|
||||
"code.gitea.io/gitea/services/auth/source/smtp"
|
||||
"code.gitea.io/gitea/services/auth/source/sspi"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/auth/pam"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
auth_service "gitea.dev/services/auth"
|
||||
"gitea.dev/services/auth/source/ldap"
|
||||
"gitea.dev/services/auth/source/oauth2"
|
||||
pam_service "gitea.dev/services/auth/source/pam"
|
||||
"gitea.dev/services/auth/source/smtp"
|
||||
"gitea.dev/services/auth/source/sspi"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -205,6 +205,7 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
|
||||
|
||||
SSHPublicKeyClaimName: form.Oauth2SSHPublicKeyClaimName,
|
||||
FullNameClaimName: form.Oauth2FullNameClaimName,
|
||||
ExternalIDClaim: form.OpenIDConnectExternalIDClaim,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,7 +440,7 @@ func EditAuthSourcePost(ctx *context.Context) {
|
||||
log.Trace("Authentication changed by admin(%s): %d", ctx.Doer.Name, source.ID)
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.auths.update_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/auths/" + strconv.FormatInt(form.ID, 10))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/auths/" + strconv.FormatInt(source.ID, 10))
|
||||
}
|
||||
|
||||
// DeleteAuthSource response for deleting an auth source
|
||||
|
||||
@@ -9,15 +9,15 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -7,20 +7,18 @@ package admin
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
system_model "code.gitea.io/gitea/models/system"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/setting/config"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/mailer"
|
||||
system_model "gitea.dev/models/system"
|
||||
"gitea.dev/modules/cache"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/setting/config"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/mailer"
|
||||
|
||||
"gitea.com/go-chi/session"
|
||||
)
|
||||
@@ -59,63 +57,6 @@ func TestCache(ctx *context.Context) {
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config")
|
||||
}
|
||||
|
||||
func shadowPasswordKV(cfgItem, splitter string) string {
|
||||
fields := strings.Split(cfgItem, splitter)
|
||||
for i := range fields {
|
||||
if strings.HasPrefix(fields[i], "password=") {
|
||||
fields[i] = "password=******"
|
||||
break
|
||||
}
|
||||
}
|
||||
return strings.Join(fields, splitter)
|
||||
}
|
||||
|
||||
func shadowURL(provider, cfgItem string) string {
|
||||
u, err := url.Parse(cfgItem)
|
||||
if err != nil {
|
||||
log.Error("Shadowing Password for %v failed: %v", provider, err)
|
||||
return cfgItem
|
||||
}
|
||||
if u.User != nil {
|
||||
atIdx := strings.Index(cfgItem, "@")
|
||||
if atIdx > 0 {
|
||||
colonIdx := strings.LastIndex(cfgItem[:atIdx], ":")
|
||||
if colonIdx > 0 {
|
||||
return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:]
|
||||
}
|
||||
}
|
||||
}
|
||||
return cfgItem
|
||||
}
|
||||
|
||||
func shadowPassword(provider, cfgItem string) string {
|
||||
switch provider {
|
||||
case "redis":
|
||||
return shadowPasswordKV(cfgItem, ",")
|
||||
case "mysql":
|
||||
// root:@tcp(localhost:3306)/macaron?charset=utf8
|
||||
atIdx := strings.Index(cfgItem, "@")
|
||||
if atIdx > 0 {
|
||||
colonIdx := strings.Index(cfgItem[:atIdx], ":")
|
||||
if colonIdx > 0 {
|
||||
return cfgItem[:colonIdx+1] + "******" + cfgItem[atIdx:]
|
||||
}
|
||||
}
|
||||
return cfgItem
|
||||
case "postgres":
|
||||
// user=jiahuachen dbname=macaron port=5432 sslmode=disable
|
||||
if !strings.HasPrefix(cfgItem, "postgres://") {
|
||||
return shadowPasswordKV(cfgItem, " ")
|
||||
}
|
||||
fallthrough
|
||||
case "couchbase":
|
||||
return shadowURL(provider, cfgItem)
|
||||
// postgres://pqgotest:password@localhost/pqgotest?sslmode=verify-full
|
||||
// Notice: use shadowURL
|
||||
}
|
||||
return cfgItem
|
||||
}
|
||||
|
||||
// Config show admin config page
|
||||
func Config(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.config_summary")
|
||||
@@ -150,8 +91,6 @@ func Config(ctx *context.Context) {
|
||||
|
||||
ctx.Data["CacheAdapter"] = setting.CacheService.Adapter
|
||||
ctx.Data["CacheInterval"] = setting.CacheService.Interval
|
||||
|
||||
ctx.Data["CacheConn"] = shadowPassword(setting.CacheService.Adapter, setting.CacheService.Conn)
|
||||
ctx.Data["CacheItemTTL"] = setting.CacheService.TTL
|
||||
|
||||
sessionCfg := setting.SessionConfig
|
||||
@@ -169,7 +108,7 @@ func Config(ctx *context.Context) {
|
||||
sessionCfg.Secure = realSession.Secure
|
||||
sessionCfg.Domain = realSession.Domain
|
||||
}
|
||||
sessionCfg.ProviderConfig = shadowPassword(sessionCfg.Provider, sessionCfg.ProviderConfig)
|
||||
sessionCfg.ProviderConfig = ""
|
||||
ctx.Data["SessionConfig"] = sessionCfg
|
||||
|
||||
ctx.Data["Git"] = setting.Git
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/tailmsg"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/modules/httplib"
|
||||
"gitea.dev/modules/tailmsg"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
func MonitorDiagnosis(ctx *context.Context) {
|
||||
|
||||
@@ -8,14 +8,14 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/user"
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/user"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -6,11 +6,11 @@ package admin
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -6,7 +6,7 @@ package admin
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"gitea.dev/models/unittest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
||||
@@ -8,12 +8,12 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
system_model "code.gitea.io/gitea/models/system"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/models/db"
|
||||
system_model "gitea.dev/models/system"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/routers/web/explore"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/routers/web/explore"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -23,10 +23,7 @@ func Organizations(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.organizations")
|
||||
ctx.Data["PageIsAdminOrganizations"] = true
|
||||
|
||||
if ctx.FormString("sort") == "" {
|
||||
ctx.SetFormString("sort", UserSearchDefaultAdminSort)
|
||||
}
|
||||
|
||||
sortOrder := ctx.FormString("sort", UserSearchDefaultAdminSort)
|
||||
explore.RenderUserSearch(ctx, user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
Types: []user_model.UserType{user_model.UserTypeOrganization},
|
||||
@@ -35,5 +32,6 @@ func Organizations(ctx *context.Context) {
|
||||
PageSize: setting.UI.Admin.OrgPagingNum,
|
||||
},
|
||||
Visible: []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate},
|
||||
OrderBy: db.SearchOrderBy(sortOrder),
|
||||
}, tplOrgs)
|
||||
}
|
||||
|
||||
@@ -8,14 +8,14 @@ import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
packages_cleanup_service "code.gitea.io/gitea/services/packages/cleanup"
|
||||
"gitea.dev/models/db"
|
||||
packages_model "gitea.dev/models/packages"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/context"
|
||||
packages_service "gitea.dev/services/packages"
|
||||
packages_cleanup_service "gitea.dev/services/packages/cleanup"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -6,8 +6,8 @@ package admin
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/tailmsg"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/modules/tailmsg"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
func PerfTrace(ctx *context.Context) {
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/modules/queue"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/modules/queue"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
func Queues(ctx *context.Context) {
|
||||
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/queue"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/process"
|
||||
"gitea.dev/modules/queue"
|
||||
"gitea.dev/modules/setting"
|
||||
)
|
||||
|
||||
var testQueueOnce sync.Once
|
||||
|
||||
@@ -8,16 +8,16 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"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/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/routers/web/explore"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/routers/web/explore"
|
||||
"gitea.dev/services/context"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/modules/process"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
func monitorTraceCommon(ctx *context.Context) {
|
||||
|
||||
@@ -11,25 +11,25 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
org_model "code.gitea.io/gitea/models/organization"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/auth/password"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/web/explore"
|
||||
user_setting "code.gitea.io/gitea/routers/web/user/setting"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/mailer"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
org_model "gitea.dev/models/organization"
|
||||
packages_model "gitea.dev/models/packages"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/auth/password"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/web/explore"
|
||||
user_setting "gitea.dev/routers/web/user/setting"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
"gitea.dev/services/mailer"
|
||||
user_service "gitea.dev/services/user"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -55,11 +55,7 @@ func Users(ctx *context.Context) {
|
||||
statusFilterMap[filterKey] = paramVal
|
||||
}
|
||||
|
||||
sortType := ctx.FormString("sort")
|
||||
if sortType == "" {
|
||||
sortType = UserSearchDefaultAdminSort
|
||||
ctx.SetFormString("sort", sortType)
|
||||
}
|
||||
sortType := ctx.FormString("sort", UserSearchDefaultAdminSort)
|
||||
ctx.PageData["adminUserListSearchForm"] = map[string]any{
|
||||
"StatusFilterMap": statusFilterMap,
|
||||
"SortType": sortType,
|
||||
@@ -78,6 +74,7 @@ func Users(ctx *context.Context) {
|
||||
IsTwoFactorEnabled: optional.ParseBool(statusFilterMap["is_2fa_enabled"]),
|
||||
IsProhibitLogin: optional.ParseBool(statusFilterMap["is_prohibit_login"]),
|
||||
IncludeReserved: true, // administrator needs to list all accounts include reserved, bot, remote ones
|
||||
OrderBy: db.SearchOrderBy(sortType),
|
||||
}, tplUsers)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ package admin
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/contexttest"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/contexttest"
|
||||
"gitea.dev/services/forms"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -7,14 +7,14 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/session"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/session"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -58,14 +58,14 @@ func TwoFactorPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the passcode with the stored TOTP secret.
|
||||
ok, err := twofa.ValidateTOTP(form.Passcode)
|
||||
// Validate the passcode and atomically consume it to prevent reuse/replay.
|
||||
ok, err := twofa.ValidateAndConsumeTOTP(ctx, form.Passcode)
|
||||
if err != nil {
|
||||
ctx.ServerError("UserSignIn", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ok && twofa.LastUsedPasscode != form.Passcode {
|
||||
if ok {
|
||||
remember := ctx.Session.Get("twofaRemember").(bool)
|
||||
u, err := user_model.GetUserByID(ctx, id)
|
||||
if err != nil {
|
||||
@@ -73,17 +73,8 @@ func TwoFactorPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Session.Get("linkAccount") != nil {
|
||||
err = linkAccountFromContext(ctx, u)
|
||||
if err != nil {
|
||||
ctx.ServerError("UserSignIn", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
twofa.LastUsedPasscode = form.Passcode
|
||||
if err = auth.UpdateTwoFactor(ctx, twofa); err != nil {
|
||||
ctx.ServerError("UserSignIn", err)
|
||||
if err = completePendingLinks(ctx, u); err != nil {
|
||||
ctx.ServerError("completePendingLinks", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -151,6 +142,11 @@ func TwoFactorScratchPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = completePendingLinks(ctx, u); err != nil {
|
||||
ctx.ServerError("completePendingLinks", err)
|
||||
return
|
||||
}
|
||||
|
||||
handleSignInFull(ctx, u, remember)
|
||||
if ctx.Written() {
|
||||
return
|
||||
|
||||
@@ -8,32 +8,33 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/auth/password"
|
||||
"code.gitea.io/gitea/modules/eventsource"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/session"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
auth_service "code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/externalaccount"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/mailer"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/auth/password"
|
||||
"gitea.dev/modules/eventsource"
|
||||
"gitea.dev/modules/httplib"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/session"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/modules/web/middleware"
|
||||
auth_service "gitea.dev/services/auth"
|
||||
"gitea.dev/services/auth/source/oauth2"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/externalaccount"
|
||||
"gitea.dev/services/forms"
|
||||
"gitea.dev/services/mailer"
|
||||
user_service "gitea.dev/services/user"
|
||||
|
||||
"github.com/markbates/goth"
|
||||
)
|
||||
@@ -118,7 +119,7 @@ func autoSignIn(ctx *context.Context) (bool, error) {
|
||||
|
||||
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
|
||||
|
||||
if err := updateSession(ctx, nil, map[string]any{
|
||||
if err := regenerateSession(ctx, nil, map[string]any{
|
||||
session.KeyUID: u.ID,
|
||||
session.KeyUname: u.Name,
|
||||
session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
|
||||
@@ -314,15 +315,6 @@ func SignInPost(ctx *context.Context) {
|
||||
log.Warn("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
|
||||
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
|
||||
ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
|
||||
} else if user_model.IsErrUserInactive(err) {
|
||||
if setting.Service.RegisterEmailConfirm {
|
||||
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
|
||||
ctx.HTML(http.StatusOK, TplActivate)
|
||||
} else {
|
||||
log.Warn("Failed authentication attempt for %s from %s: %v", form.UserName, ctx.RemoteAddr(), err)
|
||||
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
|
||||
ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
|
||||
}
|
||||
} else {
|
||||
ctx.ServerError("UserSignIn", err)
|
||||
}
|
||||
@@ -338,46 +330,35 @@ func SignInPost(ctx *context.Context) {
|
||||
|
||||
// If this user is enrolled in 2FA TOTP, we can't sign the user in just yet.
|
||||
// Instead, redirect them to the 2FA authentication page.
|
||||
hasTOTPtwofa, err := auth.HasTwoFactorByUID(ctx, u.ID)
|
||||
hasTwoFactor, err := auth.HasTwoFactorOrWebAuthn(ctx, u.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("UserSignIn", err)
|
||||
ctx.ServerError("HasTwoFactorOrWebAuthn", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the user has webauthn registration
|
||||
hasWebAuthnTwofa, err := auth.HasWebAuthnRegistrationsByUID(ctx, u.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("UserSignIn", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !hasTOTPtwofa && !hasWebAuthnTwofa {
|
||||
// No two-factor auth configured we can sign in the user
|
||||
if !hasTwoFactor {
|
||||
handleSignIn(ctx, u, form.Remember)
|
||||
return
|
||||
}
|
||||
|
||||
updates := map[string]any{
|
||||
// User will need to use 2FA TOTP or WebAuthn, save data
|
||||
"twofaUid": u.ID,
|
||||
"twofaRemember": form.Remember,
|
||||
}
|
||||
if hasTOTPtwofa {
|
||||
// User will need to use WebAuthn, save data
|
||||
updates["totpEnrolled"] = u.ID
|
||||
}
|
||||
if err := updateSession(ctx, nil, updates); err != nil {
|
||||
ctx.ServerError("UserSignIn: Unable to update session", err)
|
||||
handleTwoFactorRequired(ctx, u, form.Remember, nil)
|
||||
}
|
||||
|
||||
func handleTwoFactorRequired(ctx *context.Context, u *user_model.User, remember bool, extra map[string]any) {
|
||||
updates := map[string]any{"twofaUid": u.ID, "twofaRemember": remember}
|
||||
maps.Copy(updates, extra)
|
||||
if err := regenerateSession(ctx, nil, updates); err != nil {
|
||||
ctx.ServerError("RegenerateSession", err)
|
||||
return
|
||||
}
|
||||
|
||||
// If we have WebAuthn redirect there first
|
||||
if hasWebAuthnTwofa {
|
||||
hasWebAuthn, err := auth.HasWebAuthnRegistrationsByUID(ctx, u.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("HasWebAuthnRegistrationsByUID", err)
|
||||
return
|
||||
}
|
||||
if hasWebAuthn {
|
||||
ctx.Redirect(setting.AppSubURL + "/user/webauthn")
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback to 2FA
|
||||
ctx.Redirect(setting.AppSubURL + "/user/two_factor")
|
||||
}
|
||||
|
||||
@@ -407,7 +388,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember bool) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := updateSession(ctx, []string{
|
||||
if err := regenerateSession(ctx, []string{
|
||||
// Delete the openid, 2fa and link_account data
|
||||
"openid_verified_uri",
|
||||
"openid_signin_remember",
|
||||
@@ -417,6 +398,7 @@ func handleSignInFull(ctx *context.Context, u *user_model.User, remember bool) {
|
||||
"twofaRemember",
|
||||
"linkAccount",
|
||||
"linkAccountData",
|
||||
"openidPendingURI",
|
||||
}, map[string]any{
|
||||
session.KeyUID: u.ID,
|
||||
session.KeyUname: u.Name,
|
||||
@@ -493,20 +475,26 @@ func SignOut(ctx *context.Context) {
|
||||
}
|
||||
|
||||
func buildSignOutRedirectURL(ctx *context.Context) string {
|
||||
if ctx.Doer != nil && ctx.Doer.LoginType == auth.OAuth2 {
|
||||
if ctx.Doer != nil && shouldRedirectToOIDCEndSession(ctx) {
|
||||
if s := buildOIDCEndSessionURL(ctx, ctx.Doer); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
// The assumption is: if reverse proxy auth is enabled, then the users should only sign-in via reverse proxy auth.
|
||||
// TODO: in the future, if we need to distinguish different sign-in methods, we need to save the sign-in method in session and check here
|
||||
if setting.Service.EnableReverseProxyAuth && setting.ReverseProxyLogoutRedirect != "" {
|
||||
return setting.ReverseProxyLogoutRedirect
|
||||
}
|
||||
return setting.AppSubURL + "/"
|
||||
}
|
||||
|
||||
// shouldRedirectToOIDCEndSession reports whether this session should end at the
|
||||
// OIDC provider. Prefer the session sign-in method so an OAuth2-linked account
|
||||
// that signed in with a password does not hit end_session_endpoint.
|
||||
func shouldRedirectToOIDCEndSession(ctx *context.Context) bool {
|
||||
return ctx.Session.Get(session.KeySignInMethod) == session.SignInMethodOAuth2
|
||||
}
|
||||
|
||||
func prepareSignUpPageData(ctx *context.Context) bool {
|
||||
ctx.Data["Title"] = ctx.Tr("sign_up")
|
||||
ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up"
|
||||
@@ -636,14 +624,19 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any,
|
||||
if possibleLinkAccountData != nil && (user_model.IsErrUserAlreadyExist(err) || user_model.IsErrEmailAlreadyUsed(err)) {
|
||||
switch setting.OAuth2Client.AccountLinking {
|
||||
case setting.OAuth2AccountLinkingAuto:
|
||||
var user *user_model.User
|
||||
user = &user_model.User{Name: u.Name}
|
||||
hasUser, err := user_model.GetIndividualUser(ctx, user)
|
||||
if !hasUser || err != nil {
|
||||
user = &user_model.User{Email: u.Email}
|
||||
hasUser, err = user_model.GetIndividualUser(ctx, user)
|
||||
if !hasUser || err != nil {
|
||||
ctx.ServerError("UserLinkAccount", err)
|
||||
user, err := user_model.GetIndividualUserByName(ctx, u.Name)
|
||||
if err != nil {
|
||||
if !user_model.IsErrUserNotExist(err) {
|
||||
ctx.ServerError("GetIndividualUserByName", err)
|
||||
return false
|
||||
}
|
||||
user, err = user_model.GetIndividualUserByPrimaryEmail(ctx, u.Email)
|
||||
if err != nil {
|
||||
if !user_model.IsErrUserNotExist(err) {
|
||||
ctx.ServerError("GetIndividualUserByPrimaryEmail", err)
|
||||
} else {
|
||||
ctx.NotFound(err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -888,7 +881,7 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
|
||||
|
||||
log.Trace("User activated: %s", user.Name)
|
||||
|
||||
if err := updateSession(ctx, nil, map[string]any{
|
||||
if err := regenerateSession(ctx, nil, map[string]any{
|
||||
"uid": user.ID,
|
||||
"uname": user.Name,
|
||||
}); err != nil {
|
||||
@@ -940,7 +933,7 @@ func ActivateEmail(ctx *context.Context) {
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
|
||||
}
|
||||
|
||||
func updateSession(ctx *context.Context, deletes []string, updates map[string]any) error {
|
||||
func regenerateSession(ctx *context.Context, deletes []string, updates map[string]any) error {
|
||||
if _, err := session.RegenerateSession(ctx.Resp, ctx.Req); err != nil {
|
||||
return fmt.Errorf("regenerate session: %w", err)
|
||||
}
|
||||
|
||||
@@ -10,14 +10,15 @@ import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/session"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
"code.gitea.io/gitea/services/contexttest"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/session"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/auth/source/oauth2"
|
||||
"gitea.dev/services/contexttest"
|
||||
|
||||
"github.com/markbates/goth"
|
||||
"github.com/markbates/goth/gothic"
|
||||
@@ -154,16 +155,47 @@ func TestWebAuthOAuth2(t *testing.T) {
|
||||
authSource, err := auth_model.GetActiveOAuth2SourceByAuthName(t.Context(), "oidc-auth-source")
|
||||
require.NoError(t, err)
|
||||
|
||||
mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockMemStore("dummy-sid")}
|
||||
ctx, resp := contexttest.MockContext(t, "/user/logout", mockOpt)
|
||||
ctx.Doer = &user_model.User{ID: 1, LoginType: auth_model.OAuth2, LoginSource: authSource.ID}
|
||||
SignOut(ctx)
|
||||
assert.Equal(t, http.StatusSeeOther, resp.Code)
|
||||
u, err := url.Parse(test.RedirectURL(resp))
|
||||
require.NoError(t, err)
|
||||
expectedValues := url.Values{"oidc-key": []string{"oidc-val"}, "post_logout_redirect_uri": []string{setting.AppURL}, "client_id": []string{"mock-client-id"}}
|
||||
assert.Equal(t, expectedValues, u.Query())
|
||||
u.RawQuery = ""
|
||||
assert.Equal(t, "https://example.com/oidc-logout", u.String())
|
||||
oauthUser := &user_model.User{ID: 1, LoginType: auth_model.OAuth2, LoginSource: authSource.ID}
|
||||
|
||||
t.Run("OAuth2SignInRedirectsToOIDC", func(t *testing.T) {
|
||||
mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockMemStore("dummy-sid-oauth")}
|
||||
ctx, resp := contexttest.MockContext(t, "/user/logout", mockOpt)
|
||||
ctx.Doer = oauthUser
|
||||
require.NoError(t, ctx.Session.Set(session.KeySignInMethod, session.SignInMethodOAuth2))
|
||||
SignOut(ctx)
|
||||
assert.Equal(t, http.StatusSeeOther, resp.Code)
|
||||
u, err := url.Parse(test.RedirectURL(resp))
|
||||
require.NoError(t, err)
|
||||
expectedValues := url.Values{"oidc-key": []string{"oidc-val"}, "post_logout_redirect_uri": []string{setting.AppURL}, "client_id": []string{"mock-client-id"}}
|
||||
assert.Equal(t, expectedValues, u.Query())
|
||||
u.RawQuery = ""
|
||||
assert.Equal(t, "https://example.com/oidc-logout", u.String())
|
||||
})
|
||||
|
||||
t.Run("PasswordSignInSkipsOIDC", func(t *testing.T) {
|
||||
// OAuth2-linked account signed in via password form must not hit end_session_endpoint.
|
||||
mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockMemStore("dummy-sid-password")}
|
||||
ctx, resp := contexttest.MockContext(t, "/user/logout", mockOpt)
|
||||
ctx.Doer = oauthUser
|
||||
SignOut(ctx)
|
||||
assert.Equal(t, http.StatusSeeOther, resp.Code)
|
||||
assert.Equal(t, "/", test.RedirectURL(resp))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOpenIDRequireTwoFactor(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockMemStore("dummy-sid-openid")}
|
||||
|
||||
user32 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 32}) // has a webauthn credential
|
||||
ctx, resp := contexttest.MockContext(t, "/user/openid/connect", mockOpt)
|
||||
openIDRequireTwoFactor(ctx, user32, false, "https://example.com/id")
|
||||
assert.Equal(t, "/user/webauthn", test.RedirectURL(resp))
|
||||
unittest.AssertNotExistsBean(t, &user_model.UserOpenID{UID: user32.ID}) // not attached before the key answered
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
ctx, _ = contexttest.MockContext(t, "/user/openid/connect", mockOpt)
|
||||
openIDRequireTwoFactor(ctx, user2, false, "https://example.com/id")
|
||||
assert.False(t, ctx.Written())
|
||||
}
|
||||
|
||||
@@ -8,18 +8,19 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
auth_service "code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/externalaccount"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/session"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
auth_service "gitea.dev/services/auth"
|
||||
"gitea.dev/services/auth/source/oauth2"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/externalaccount"
|
||||
"gitea.dev/services/forms"
|
||||
)
|
||||
|
||||
var tplLinkAccount templates.TplName = "user/auth/link_account"
|
||||
@@ -105,16 +106,6 @@ func handleSignInError(ctx *context.Context, userName string, ptrForm any, tmpl
|
||||
log.Info("Failed authentication attempt for %s from %s: %v", userName, ctx.RemoteAddr(), err)
|
||||
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
|
||||
ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
|
||||
} else if user_model.IsErrUserInactive(err) {
|
||||
ctx.Data["user_exists"] = true
|
||||
if setting.Service.RegisterEmailConfirm {
|
||||
ctx.Data["Title"] = ctx.Tr("auth.active_your_account")
|
||||
ctx.HTML(http.StatusOK, TplActivate)
|
||||
} else {
|
||||
log.Info("Failed authentication attempt for %s from %s: %v", userName, ctx.RemoteAddr(), err)
|
||||
ctx.Data["Title"] = ctx.Tr("auth.prohibit_login")
|
||||
ctx.HTML(http.StatusOK, "user/auth/prohibit_login")
|
||||
}
|
||||
} else {
|
||||
ctx.ServerError(invoker, err)
|
||||
}
|
||||
@@ -157,15 +148,13 @@ func oauth2LinkAccount(ctx *context.Context, u *user_model.User, linkAccountData
|
||||
// If this user is enrolled in 2FA, we can't sign the user in just yet.
|
||||
// Instead, redirect them to the 2FA authentication page.
|
||||
// We deliberately ignore the skip local 2fa setting here because we are linking to a previous user here
|
||||
_, err := auth.GetTwoFactorByUID(ctx, u.ID)
|
||||
hasTwoFactor, err := auth.HasTwoFactorOrWebAuthn(ctx, u.ID)
|
||||
if err != nil {
|
||||
if !auth.IsErrTwoFactorNotEnrolled(err) {
|
||||
ctx.ServerError("UserLinkAccount", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = externalaccount.LinkAccountToUser(ctx, linkAccountData.AuthSourceID, u, linkAccountData.GothUser)
|
||||
if err != nil {
|
||||
ctx.ServerError("UserLinkAccount", err)
|
||||
return
|
||||
}
|
||||
if !hasTwoFactor {
|
||||
if err := externalaccount.LinkAccountToUser(ctx, linkAccountData.AuthSourceID, u, linkAccountData.GothUser); err != nil {
|
||||
ctx.ServerError("UserLinkAccount", err)
|
||||
return
|
||||
}
|
||||
@@ -174,24 +163,15 @@ func oauth2LinkAccount(ctx *context.Context, u *user_model.User, linkAccountData
|
||||
return
|
||||
}
|
||||
|
||||
if err := updateSession(ctx, nil, map[string]any{
|
||||
// User needs to use 2FA, save data and redirect to 2FA page.
|
||||
"twofaUid": u.ID,
|
||||
"twofaRemember": remember,
|
||||
"linkAccount": true,
|
||||
}); err != nil {
|
||||
ctx.ServerError("RegenerateSession", err)
|
||||
if err := Oauth2SetLinkAccountData(ctx, *linkAccountData); err != nil {
|
||||
ctx.ServerError("Oauth2SetLinkAccountData", err)
|
||||
return
|
||||
}
|
||||
|
||||
// If WebAuthn is enrolled -> Redirect to WebAuthn instead
|
||||
regs, err := auth.GetWebAuthnCredentialsByUID(ctx, u.ID)
|
||||
if err == nil && len(regs) > 0 {
|
||||
ctx.Redirect(setting.AppSubURL + "/user/webauthn")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(setting.AppSubURL + "/user/two_factor")
|
||||
handleTwoFactorRequired(ctx, u, remember, map[string]any{
|
||||
"linkAccount": true,
|
||||
session.KeySignInMethod: session.SignInMethodOAuth2,
|
||||
})
|
||||
}
|
||||
|
||||
// LinkAccountPostRegister handle the creation of a new account for an external account using signUp
|
||||
@@ -282,6 +262,15 @@ func LinkAccountPostRegister(ctx *context.Context) {
|
||||
handleSignIn(ctx, u, false)
|
||||
}
|
||||
|
||||
func completePendingLinks(ctx *context.Context, user *user_model.User) error {
|
||||
if ctx.Session.Get("linkAccount") != nil {
|
||||
if err := linkAccountFromContext(ctx, user); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return openIDConnectFromContext(ctx, user)
|
||||
}
|
||||
|
||||
func linkAccountFromContext(ctx *context.Context, user *user_model.User) error {
|
||||
linkAccountData := oauth2GetLinkAccountData(ctx)
|
||||
if linkAccountData == nil {
|
||||
|
||||
@@ -6,7 +6,7 @@ package auth
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"gitea.dev/models/unittest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
||||
@@ -15,20 +15,22 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
auth_module "code.gitea.io/gitea/modules/auth"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/session"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
source_service "code.gitea.io/gitea/services/auth/source"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/externalaccount"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
"gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
auth_module "gitea.dev/modules/auth"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/hostmatcher"
|
||||
"gitea.dev/modules/httplib"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/proxy"
|
||||
"gitea.dev/modules/session"
|
||||
"gitea.dev/modules/setting"
|
||||
source_service "gitea.dev/services/auth/source"
|
||||
"gitea.dev/services/auth/source/oauth2"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/externalaccount"
|
||||
user_service "gitea.dev/services/user"
|
||||
|
||||
"github.com/markbates/goth"
|
||||
"github.com/markbates/goth/gothic"
|
||||
@@ -80,7 +82,7 @@ func SignInOAuthCallback(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
sort.Strings(errorKeyValues)
|
||||
ctx.Flash.Error(strings.Join(errorKeyValues, "<br>"), true)
|
||||
ctx.Flash.Error(strings.Join(errorKeyValues, "\n"), true)
|
||||
}
|
||||
|
||||
// first look if the provider is still active
|
||||
@@ -188,10 +190,6 @@ func SignInOAuthCallback(ctx *context.Context) {
|
||||
|
||||
source := authSource.Cfg.(*oauth2.Source)
|
||||
|
||||
isAdmin, isRestricted := getUserAdminAndRestrictedFromGroupClaims(source, &gothUser)
|
||||
u.IsAdmin = isAdmin.ValueOrDefault(user_service.UpdateOptionField[bool]{FieldValue: false}).FieldValue
|
||||
u.IsRestricted = isRestricted.ValueOrDefault(setting.Service.DefaultUserIsRestricted)
|
||||
|
||||
linkAccountData := &LinkAccountData{authSource.ID, gothUser}
|
||||
if setting.OAuth2Client.AccountLinking == setting.OAuth2AccountLinkingDisabled {
|
||||
linkAccountData = nil
|
||||
@@ -289,9 +287,7 @@ func oauth2GetLinkAccountData(ctx *context.Context) *LinkAccountData {
|
||||
}
|
||||
|
||||
func Oauth2SetLinkAccountData(ctx *context.Context, linkAccountData LinkAccountData) error {
|
||||
return updateSession(ctx, nil, map[string]any{
|
||||
"linkAccountData": linkAccountData,
|
||||
})
|
||||
return ctx.Session.Set("linkAccountData", linkAccountData)
|
||||
}
|
||||
|
||||
func showLinkingLogin(ctx *context.Context, authSourceID int64, gothUser goth.User) {
|
||||
@@ -302,7 +298,23 @@ func showLinkingLogin(ctx *context.Context, authSourceID int64, gothUser goth.Us
|
||||
ctx.Redirect(setting.AppSubURL + "/user/link_account")
|
||||
}
|
||||
|
||||
var oauth2AvatarHTTPClient = &http.Client{Timeout: 30 * time.Second}
|
||||
// oauth2AvatarAllowList parses the host allow-list applied to avatar fetches from the global
|
||||
// [security] ALLOWED_HOST_LIST, defaulting an empty setting to the built-in "external" set. An empty
|
||||
// host-match list would otherwise disable the allow-list check entirely and permit any host, including
|
||||
// loopback/private addresses (SSRF).
|
||||
func oauth2AvatarAllowList() *hostmatcher.HostMatchList {
|
||||
return hostmatcher.ParseHostMatchList("security.ALLOWED_HOST_LIST", setting.Security.AllowedHostList)
|
||||
}
|
||||
|
||||
// oauth2AvatarHTTPClient builds the SSRF-protected client for avatar fetches. It is constructed per call
|
||||
// so a changed allowlist takes effect (avatar fetches are infrequent, so this is not a hot path).
|
||||
func oauth2AvatarHTTPClient() *http.Client {
|
||||
allowList := oauth2AvatarAllowList()
|
||||
return &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: hostmatcher.NewHTTPTransport("oauth2-avatar", allowList, nil, proxy.Proxy(), setting.Proxy.ProxyURLFixed, nil),
|
||||
}
|
||||
}
|
||||
|
||||
func oauth2UpdateAvatarIfNeed(ctx *context.Context, avatarURL string, u *user_model.User) {
|
||||
if !setting.OAuth2Client.UpdateAvatar || len(avatarURL) == 0 {
|
||||
@@ -316,7 +328,7 @@ func oauth2UpdateAvatarIfNeed(ctx *context.Context, avatarURL string, u *user_mo
|
||||
// Some hosts (e.g. Wikimedia) reject Go's default User-Agent.
|
||||
req.Header.Set("User-Agent", "Gitea "+setting.AppVer)
|
||||
|
||||
resp, err := oauth2AvatarHTTPClient.Do(req)
|
||||
resp, err := oauth2AvatarHTTPClient().Do(req)
|
||||
if err != nil {
|
||||
log.Warn("fetch %q failed: %v", avatarURL, err)
|
||||
return
|
||||
@@ -349,12 +361,11 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m
|
||||
|
||||
needs2FA := false
|
||||
if !authSource.TwoFactorShouldSkip() {
|
||||
_, err := auth.GetTwoFactorByUID(ctx, u.ID)
|
||||
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
|
||||
var err error
|
||||
if needs2FA, err = auth.HasTwoFactorOrWebAuthn(ctx, u.ID); err != nil {
|
||||
ctx.ServerError("UserSignIn", err)
|
||||
return
|
||||
}
|
||||
needs2FA = err == nil
|
||||
}
|
||||
|
||||
oauth2Source := authSource.Cfg.(*oauth2.Source)
|
||||
@@ -368,14 +379,26 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m
|
||||
|
||||
opts := &user_service.UpdateOptions{}
|
||||
|
||||
// Reactivate user if they are deactivated
|
||||
// HINT: OAUTH-AUTO-SYNC-USER-ACTIVATION: see services/auth/source/oauth2/source_sync.go
|
||||
// Reactivate user only if they were disabled by the OAuth2 auto sync cron (invalid_grant),
|
||||
// which clears AccessToken/RefreshToken/ExpiresAt on the ExternalLoginUser row
|
||||
// An admin-disabled user has no such signature, so we leave IsActive alone
|
||||
// and let verifyAuthWithOptions route them through the prohibit-login / activate page.
|
||||
if !u.IsActive {
|
||||
opts.IsActive = optional.Some(true)
|
||||
extLogin, hasExt, err := user_model.GetExternalLogin(ctx, authSource.ID, gothUser.UserID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetExternalLogin", err)
|
||||
return
|
||||
}
|
||||
// the cron clears all three token fields when it disables a user, so require the
|
||||
// full signature; a RefreshToken alone is empty for many normal logins (e.g. GitHub
|
||||
// or OIDC without offline_access), which would otherwise reactivate admin-disabled users
|
||||
isDisabledByAutoSync := hasExt && extLogin.AccessToken == "" && extLogin.RefreshToken == "" && extLogin.ExpiresAt.IsZero()
|
||||
if isDisabledByAutoSync {
|
||||
opts.IsActive = optional.Some(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Update GroupClaims
|
||||
opts.IsAdmin, opts.IsRestricted = getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
|
||||
|
||||
if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval {
|
||||
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil {
|
||||
ctx.ServerError("SyncGroupsToTeams", err)
|
||||
@@ -404,10 +427,11 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m
|
||||
return
|
||||
}
|
||||
|
||||
if err := updateSession(ctx, nil, map[string]any{
|
||||
if err := regenerateSession(ctx, nil, map[string]any{
|
||||
session.KeyUID: u.ID,
|
||||
session.KeyUname: u.Name,
|
||||
session.KeyUserHasTwoFactorAuth: userHasTwoFactorAuth,
|
||||
session.KeySignInMethod: session.SignInMethodOAuth2,
|
||||
}); err != nil {
|
||||
ctx.ServerError("updateSession", err)
|
||||
return
|
||||
@@ -429,23 +453,7 @@ func handleOAuth2SignIn(ctx *context.Context, authSource *auth.Source, u *user_m
|
||||
}
|
||||
}
|
||||
|
||||
if err := updateSession(ctx, nil, map[string]any{
|
||||
// User needs to use 2FA, save data and redirect to 2FA page.
|
||||
"twofaUid": u.ID,
|
||||
"twofaRemember": false,
|
||||
}); err != nil {
|
||||
ctx.ServerError("updateSession", err)
|
||||
return
|
||||
}
|
||||
|
||||
// If WebAuthn is enrolled -> Redirect to WebAuthn instead
|
||||
regs, err := auth.GetWebAuthnCredentialsByUID(ctx, u.ID)
|
||||
if err == nil && len(regs) > 0 {
|
||||
ctx.Redirect(setting.AppSubURL + "/user/webauthn")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(setting.AppSubURL + "/user/two_factor")
|
||||
handleTwoFactorRequired(ctx, u, false, map[string]any{session.KeySignInMethod: session.SignInMethodOAuth2})
|
||||
}
|
||||
|
||||
// OAuth2UserLoginCallback attempts to handle the callback from the OAuth2 provider and if successful
|
||||
@@ -498,13 +506,7 @@ func oAuth2UserLoginCallback(ctx *context.Context, authSource *auth.Source, requ
|
||||
}
|
||||
}
|
||||
|
||||
user := &user_model.User{
|
||||
LoginName: gothUser.UserID,
|
||||
LoginType: auth.OAuth2,
|
||||
LoginSource: authSource.ID,
|
||||
}
|
||||
|
||||
hasUser, err := user_model.GetIndividualUser(ctx, user)
|
||||
user, hasUser, err := user_model.GetIndividualUserByLoginSource(ctx, auth.OAuth2, authSource.ID, gothUser.UserID)
|
||||
if err != nil {
|
||||
return nil, goth.User{}, err
|
||||
}
|
||||
@@ -514,17 +516,33 @@ func oAuth2UserLoginCallback(ctx *context.Context, authSource *auth.Source, requ
|
||||
}
|
||||
|
||||
// search in external linked users
|
||||
externalLoginUser := &user_model.ExternalLoginUser{
|
||||
ExternalID: gothUser.UserID,
|
||||
LoginSourceID: authSource.ID,
|
||||
}
|
||||
hasUser, err = user_model.GetExternalLogin(request.Context(), externalLoginUser)
|
||||
externalLoginUser, hasUser, err := user_model.GetExternalLogin(ctx, authSource.ID, gothUser.UserID)
|
||||
if err != nil {
|
||||
return nil, goth.User{}, err
|
||||
}
|
||||
if hasUser {
|
||||
user, err = user_model.GetUserByID(request.Context(), externalLoginUser.UserID)
|
||||
return user, gothUser, err
|
||||
user, err = user_model.GetUserByID(ctx, externalLoginUser.UserID)
|
||||
if err != nil && !user_model.IsErrUserNotExist(err) {
|
||||
return nil, goth.User{}, err
|
||||
}
|
||||
if err == nil && user.IsIndividual() {
|
||||
return user, gothUser, nil
|
||||
}
|
||||
|
||||
// The external login record is stale: the linked user no longer exists, or it exists but is
|
||||
// not an individual user (only individual users can sign in, so a link pointing at an
|
||||
// organization, bot or remote user can never resolve). Remove it so the next sign-in can
|
||||
// relink the external account to the correct user. Nothing is lost, because the link is
|
||||
// recreated automatically on the next sign-in.
|
||||
reason := "linked user does not exist"
|
||||
if err == nil {
|
||||
reason = fmt.Sprintf("linked user type is %d", user.Type)
|
||||
}
|
||||
log.Warn("Ignoring stale external login link [external-id=%s login-source-id=%d user-id=%d]: %s", externalLoginUser.ExternalID, externalLoginUser.LoginSourceID, externalLoginUser.UserID, reason)
|
||||
|
||||
if err := user_model.RemoveExternalLoginByExternalID(ctx, externalLoginUser.LoginSourceID, externalLoginUser.ExternalID); err != nil {
|
||||
return nil, goth.User{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// no user found to login
|
||||
@@ -561,7 +579,15 @@ func buildOIDCEndSessionURL(ctx *context.Context, doer *user_model.User) string
|
||||
// https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout
|
||||
params := endSessionURL.Query()
|
||||
params.Set("client_id", oauth2Cfg.ClientID)
|
||||
params.Set("post_logout_redirect_uri", httplib.GuessCurrentAppURL(ctx))
|
||||
|
||||
// AWS Cognito uses "logout_uri" instead of the standard "post_logout_redirect_uri"
|
||||
redirectURI := httplib.GuessCurrentAppURL(ctx)
|
||||
if oauth2Cfg.Provider == oauth2.ProviderNameAwsCognito {
|
||||
params.Set("logout_uri", redirectURI)
|
||||
} else {
|
||||
params.Set("post_logout_redirect_uri", redirectURI)
|
||||
}
|
||||
|
||||
endSessionURL.RawQuery = params.Encode()
|
||||
return endSessionURL.String()
|
||||
}
|
||||
|
||||
@@ -13,18 +13,18 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/auth/httpauth"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
auth_service "code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/oauth2_provider"
|
||||
"gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/auth/httpauth"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/web"
|
||||
auth_service "gitea.dev/services/auth"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
"gitea.dev/services/oauth2_provider"
|
||||
|
||||
"gitea.com/go-chi/binding"
|
||||
jwt "github.com/golang-jwt/jwt/v5"
|
||||
@@ -98,6 +98,19 @@ func InfoOAuth(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// enforce the same user scope the REST API requires before returning identity
|
||||
// claims; OIDC access tokens map to the "all" scope, so standard OIDC clients
|
||||
// are unaffected and only explicitly-restricted tokens are rejected
|
||||
tokenScope, _ := ctx.Data["ApiTokenScope"].(auth.AccessTokenScope)
|
||||
if allowed, err := tokenScope.HasScope(auth.AccessTokenScopeReadUser); err != nil {
|
||||
ctx.ServerError("HasScope", err)
|
||||
return
|
||||
} else if !allowed {
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Bearer realm="Gitea OAuth2"`)
|
||||
ctx.PlainText(http.StatusForbidden, "token does not have required scope: read:user")
|
||||
return
|
||||
}
|
||||
|
||||
response := &userInfoResponse{
|
||||
Sub: strconv.FormatInt(ctx.Doer.ID, 10),
|
||||
Name: ctx.Doer.DisplayName(),
|
||||
@@ -128,7 +141,7 @@ func InfoOAuth(ctx *context.Context) {
|
||||
|
||||
// IntrospectOAuth introspects an oauth token
|
||||
func IntrospectOAuth(ctx *context.Context) {
|
||||
clientIDValid := false
|
||||
var introspectingApp *auth.OAuth2Application
|
||||
authHeader := ctx.Req.Header.Get("Authorization")
|
||||
if parsed, ok := httpauth.ParseAuthorizationHeader(authHeader); ok && parsed.BasicAuth != nil {
|
||||
clientID, clientSecret := parsed.BasicAuth.Username, parsed.BasicAuth.Password
|
||||
@@ -139,9 +152,14 @@ func IntrospectOAuth(ctx *context.Context) {
|
||||
ctx.HTTPError(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
clientIDValid = err == nil && app.ValidateClientSecret([]byte(clientSecret))
|
||||
clientIDValid := err == nil && app.ValidateClientSecret([]byte(clientSecret))
|
||||
if clientIDValid {
|
||||
introspectingApp = app
|
||||
}
|
||||
}
|
||||
if !clientIDValid {
|
||||
if introspectingApp == nil {
|
||||
// RFC 7662 requires the caller to authenticate to the introspection endpoint.
|
||||
// https://www.rfc-editor.org/rfc/rfc7662.html#section-2.1
|
||||
ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea OAuth2"`)
|
||||
ctx.PlainText(http.StatusUnauthorized, "no valid authorization")
|
||||
return
|
||||
@@ -156,27 +174,57 @@ func IntrospectOAuth(ctx *context.Context) {
|
||||
|
||||
form := web.GetForm(ctx).(*forms.IntrospectTokenForm)
|
||||
token, err := oauth2_provider.ParseToken(form.Token, oauth2_provider.DefaultSigningKey)
|
||||
if err == nil {
|
||||
grant, err := auth.GetOAuth2GrantByID(ctx, token.GrantID)
|
||||
if err == nil && grant != nil {
|
||||
app, err := auth.GetOAuth2ApplicationByID(ctx, grant.ApplicationID)
|
||||
if err == nil && app != nil {
|
||||
response.Active = true
|
||||
response.Scope = grant.Scope
|
||||
response.RegisteredClaims = oauth2_provider.NewJwtRegisteredClaimsFromUser(app.ClientID, grant.UserID, nil /*exp*/)
|
||||
}
|
||||
if user, err := user_model.GetUserByID(ctx, grant.UserID); err == nil {
|
||||
response.Username = user.Name
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
// RFC 7662 returns inactive token metadata for invalid/unknown tokens.
|
||||
// https://www.rfc-editor.org/rfc/rfc7662.html#section-2.2
|
||||
log.Trace("Ignoring invalid token during introspection: %v", err)
|
||||
ctx.JSON(http.StatusOK, response)
|
||||
return
|
||||
}
|
||||
|
||||
grant, err := auth.GetOAuth2GrantByID(ctx, token.GrantID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetOAuth2GrantByID", err)
|
||||
return
|
||||
}
|
||||
if grant == nil || grant.ApplicationID != introspectingApp.ID {
|
||||
// RFC 7662 allows the server to reply inactive when the caller must not learn more.
|
||||
// https://www.rfc-editor.org/rfc/rfc7662.html#section-2.2
|
||||
ctx.JSON(http.StatusOK, response)
|
||||
return
|
||||
}
|
||||
|
||||
response.Active = true
|
||||
response.Scope = grant.Scope
|
||||
response.RegisteredClaims = oauth2_provider.NewJwtRegisteredClaimsFromUser(introspectingApp.ClientID, grant.UserID, nil /*exp*/)
|
||||
user, err := user_model.GetUserByID(ctx, grant.UserID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserByID", err)
|
||||
return
|
||||
}
|
||||
response.Username = user.Name
|
||||
|
||||
ctx.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func oauthDoerAuthorizePreCheck(ctx *context.Context, formState string) bool {
|
||||
if ctx.DoerNeedTwoFactorAuth() {
|
||||
handleAuthorizeError(ctx, AuthorizeError{
|
||||
ErrorCode: ErrorCodeAccessDenied,
|
||||
ErrorDescription: "two-factor authentication is required",
|
||||
State: formState,
|
||||
}, "")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// AuthorizeOAuth manages authorize requests
|
||||
func AuthorizeOAuth(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.AuthorizationForm)
|
||||
if !oauthDoerAuthorizePreCheck(ctx, form.State) {
|
||||
return
|
||||
}
|
||||
errs := binding.Errors{}
|
||||
errs = form.Validate(ctx.Req, errs)
|
||||
if len(errs) > 0 {
|
||||
@@ -352,6 +400,10 @@ func AuthorizeOAuth(ctx *context.Context) {
|
||||
// GrantApplicationOAuth manages the post request submitted when a user grants access to an application
|
||||
func GrantApplicationOAuth(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.GrantApplicationForm)
|
||||
if !oauthDoerAuthorizePreCheck(ctx, form.State) {
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Session.Get("client_id") != form.ClientID || ctx.Session.Get("state") != form.State ||
|
||||
ctx.Session.Get("redirect_uri") != form.RedirectURI {
|
||||
ctx.HTTPError(http.StatusBadRequest)
|
||||
|
||||
@@ -6,14 +6,15 @@ package auth
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
asymkey_model "gitea.dev/models/asymkey"
|
||||
"gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/util"
|
||||
asymkey_service "gitea.dev/services/asymkey"
|
||||
"gitea.dev/services/auth/source/oauth2"
|
||||
"gitea.dev/services/context"
|
||||
user_service "gitea.dev/services/user"
|
||||
|
||||
"github.com/markbates/goth"
|
||||
)
|
||||
@@ -50,6 +51,14 @@ func oauth2SignInSync(ctx *context.Context, authSourceID int64, u *user_model.Us
|
||||
}
|
||||
}
|
||||
|
||||
// sync user flags (admin/restricted)
|
||||
isAdmin, isRestricted := getUserAdminAndRestrictedFromGroupClaims(oauth2Source, &gothUser)
|
||||
if isAdmin.Has() || isRestricted.Has() {
|
||||
if err = user_service.UpdateUser(ctx, u, &user_service.UpdateOptions{IsAdmin: isAdmin, IsRestricted: isRestricted}); err != nil {
|
||||
log.Error("Unable to sync OAuth2 user admin or restricted status %s: %v", gothUser.Provider, err)
|
||||
}
|
||||
}
|
||||
|
||||
err = oauth2UpdateSSHPubIfNeed(ctx, authSource, &gothUser, u)
|
||||
if err != nil {
|
||||
log.Error("Unable to sync OAuth2 SSH public key %s: %v", gothUser.Provider, err)
|
||||
|
||||
@@ -4,15 +4,22 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/services/oauth2_provider"
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/hostmatcher"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/services/oauth2_provider"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func createAndParseToken(t *testing.T, grant *auth.OAuth2Grant) *oauth2_provider.OIDCToken {
|
||||
@@ -73,3 +80,42 @@ func TestNewAccessTokenResponse_OIDCToken(t *testing.T) {
|
||||
assert.Equal(t, user.Email, oidcToken.Email)
|
||||
assert.Equal(t, user.IsActive, oidcToken.EmailVerified)
|
||||
}
|
||||
|
||||
func TestOAuth2AvatarClientBlocksLoopback(t *testing.T) {
|
||||
var hit atomic.Bool
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
hit.Store(true)
|
||||
_, _ = w.Write([]byte("img"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
// the httptest server binds a loopback address, which the SSRF-protected dialer must refuse
|
||||
resp, err := oauth2AvatarHTTPClient().Get(srv.URL)
|
||||
if resp != nil {
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
require.Error(t, err)
|
||||
assert.False(t, hit.Load(), "avatar client must refuse to dial a loopback address")
|
||||
}
|
||||
|
||||
func TestOAuth2AvatarAllowListRestricts(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Security.AllowedHostList, "avatars.example.com")()
|
||||
allowList := oauth2AvatarAllowList()
|
||||
assert.True(t, allowList.MatchHostName("avatars.example.com"), "the configured host must be allowed")
|
||||
assert.False(t, allowList.MatchHostName("8.8.8.8"), "an unrelated external host must be rejected")
|
||||
|
||||
// the default `external` allow-list still permits external hosts
|
||||
setting.Security.AllowedHostList = hostmatcher.MatchBuiltinExternal
|
||||
assert.True(t, oauth2AvatarAllowList().MatchHostName("8.8.8.8"), "default allow-list permits external hosts")
|
||||
}
|
||||
|
||||
func TestOAuth2AvatarClientBlocksCloudMetadata(t *testing.T) {
|
||||
// external-only allow-list must reject link-local cloud metadata (169.254.169.254) at dial time
|
||||
resp, err := oauth2AvatarHTTPClient().Get("http://169.254.169.254/latest/meta-data/")
|
||||
if resp != nil {
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, "can only call allowed HTTP servers",
|
||||
"avatar client must refuse a link-local cloud-metadata address")
|
||||
}
|
||||
|
||||
@@ -8,16 +8,17 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/auth/openid"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/auth/openid"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/auth"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,6 +27,36 @@ const (
|
||||
tplSignUpOID templates.TplName = "user/auth/signup_openid_register"
|
||||
)
|
||||
|
||||
// the OpenID is attached only after the second factor passed, so a stolen password cannot leave one behind
|
||||
func openIDRequireTwoFactor(ctx *context.Context, u *user_model.User, remember bool, pendingURI string) {
|
||||
hasTwoFactor, err := auth_model.HasTwoFactorOrWebAuthn(ctx, u.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("HasTwoFactorOrWebAuthn", err)
|
||||
return
|
||||
}
|
||||
if !hasTwoFactor {
|
||||
return
|
||||
}
|
||||
handleTwoFactorRequired(ctx, u, remember, map[string]any{"openidPendingURI": pendingURI})
|
||||
}
|
||||
|
||||
func openIDConnectFromContext(ctx *context.Context, u *user_model.User) error {
|
||||
uri, _ := ctx.Session.Get("openidPendingURI").(string)
|
||||
if uri == "" {
|
||||
return nil
|
||||
}
|
||||
if err := ctx.Session.Delete("openidPendingURI"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := user_model.AddUserOpenID(ctx, &user_model.UserOpenID{UID: u.ID, URI: uri}); err != nil {
|
||||
if !user_model.IsErrOpenIDAlreadyUsed(err) {
|
||||
return err
|
||||
}
|
||||
ctx.Flash.Error(ctx.Tr("form.openid_been_used", uri))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SignInOpenID render sign in page
|
||||
func SignInOpenID(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("sign_in")
|
||||
@@ -154,6 +185,10 @@ func signInOpenIDVerify(ctx *context.Context) {
|
||||
log.Trace("User exists, logging in")
|
||||
remember, _ := ctx.Session.Get("openid_signin_remember").(bool)
|
||||
log.Trace("Session stored openid-remember: %t", remember)
|
||||
openIDRequireTwoFactor(ctx, u, remember, "")
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
handleSignIn(ctx, u, remember)
|
||||
return
|
||||
}
|
||||
@@ -213,7 +248,7 @@ func signInOpenIDVerify(ctx *context.Context) {
|
||||
if u != nil {
|
||||
nickname = u.LowerName
|
||||
}
|
||||
if err := updateSession(ctx, nil, map[string]any{
|
||||
if err := regenerateSession(ctx, nil, map[string]any{
|
||||
"openid_verified_uri": id,
|
||||
"openid_determined_email": email,
|
||||
"openid_determined_username": nickname,
|
||||
@@ -270,9 +305,14 @@ func ConnectOpenIDPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// add OpenID for the user
|
||||
remember, _ := ctx.Session.Get("openid_signin_remember").(bool)
|
||||
openIDRequireTwoFactor(ctx, u, remember, oid)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
userOID := &user_model.UserOpenID{UID: u.ID, URI: oid}
|
||||
if err = user_model.AddUserOpenID(ctx, userOID); err != nil {
|
||||
if err := user_model.AddUserOpenID(ctx, userOID); err != nil {
|
||||
if user_model.IsErrOpenIDAlreadyUsed(err) {
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.openid_been_used", oid), tplConnectOID, &form)
|
||||
return
|
||||
@@ -282,9 +322,6 @@ func ConnectOpenIDPost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("settings.add_openid_success"))
|
||||
|
||||
remember, _ := ctx.Session.Get("openid_signin_remember").(bool)
|
||||
log.Trace("Session stored openid-remember: %t", remember)
|
||||
handleSignIn(ctx, u, remember)
|
||||
}
|
||||
|
||||
@@ -345,11 +382,7 @@ func RegisterOpenIDPost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
length := max(setting.MinPasswordLength, 256)
|
||||
password, err := util.CryptoRandomString(int64(length))
|
||||
if err != nil {
|
||||
ctx.RenderWithErrDeprecated(err.Error(), tplSignUpOID, form)
|
||||
return
|
||||
}
|
||||
password := util.CryptoRandomString(int64(length))
|
||||
|
||||
u := &user_model.User{
|
||||
Name: form.UserName,
|
||||
@@ -363,7 +396,7 @@ func RegisterOpenIDPost(ctx *context.Context) {
|
||||
|
||||
// add OpenID for the user
|
||||
userOID := &user_model.UserOpenID{UID: u.ID, URI: oid}
|
||||
if err = user_model.AddUserOpenID(ctx, userOID); err != nil {
|
||||
if err := user_model.AddUserOpenID(ctx, userOID); err != nil {
|
||||
if user_model.IsErrOpenIDAlreadyUsed(err) {
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.openid_been_used", oid), tplSignUpOID, &form)
|
||||
return
|
||||
|
||||
@@ -7,19 +7,19 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/auth/password"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/mailer"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
"gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/auth/password"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
"gitea.dev/services/mailer"
|
||||
user_service "gitea.dev/services/user"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -177,23 +177,17 @@ func ResetPasswdPost(ctx *context.Context) {
|
||||
regenerateScratchToken = true
|
||||
} else {
|
||||
passcode := ctx.FormString("passcode")
|
||||
ok, err := twofa.ValidateTOTP(passcode)
|
||||
ok, err := twofa.ValidateAndConsumeTOTP(ctx, passcode)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "ValidateTOTP", err.Error())
|
||||
ctx.HTTPError(http.StatusInternalServerError, "ValidateAndConsumeTOTP", err.Error())
|
||||
return
|
||||
}
|
||||
if !ok || twofa.LastUsedPasscode == passcode {
|
||||
if !ok {
|
||||
ctx.Data["IsResetForm"] = true
|
||||
ctx.Data["Err_Passcode"] = true
|
||||
ctx.RenderWithErrDeprecated(ctx.Tr("auth.twofa_passcode_incorrect"), tplResetPassword, nil)
|
||||
return
|
||||
}
|
||||
|
||||
twofa.LastUsedPasscode = passcode
|
||||
if err = auth.UpdateTwoFactor(ctx, twofa); err != nil {
|
||||
ctx.ServerError("ResetPasswdPost: UpdateTwoFactor", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,6 +238,19 @@ func ResetPasswdPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// the reset form only carries a TOTP field, so a WebAuthn-only user finishes on its own page
|
||||
if twofa == nil {
|
||||
hasWebAuthn, err := auth.HasWebAuthnRegistrationsByUID(ctx, u.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("HasWebAuthnRegistrationsByUID", err)
|
||||
return
|
||||
}
|
||||
if hasWebAuthn {
|
||||
handleTwoFactorRequired(ctx, u, remember, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
handleSignIn(ctx, u, remember)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
wa "code.gitea.io/gitea/modules/auth/webauthn"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/models/auth"
|
||||
user_model "gitea.dev/models/user"
|
||||
wa "gitea.dev/modules/auth/webauthn"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/context"
|
||||
|
||||
"github.com/go-webauthn/webauthn/protocol"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
@@ -54,7 +54,8 @@ func WebAuthnPasskeyAssertion(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
assertion, sessionData, err := wa.WebAuthn.BeginDiscoverableLogin()
|
||||
// a passkey is the only factor here
|
||||
assertion, sessionData, err := wa.WebAuthn.BeginDiscoverableLogin(webauthn.WithUserVerification(protocol.VerificationRequired))
|
||||
if err != nil {
|
||||
ctx.ServerError("webauthn.BeginDiscoverableLogin", err)
|
||||
return
|
||||
@@ -91,7 +92,7 @@ func WebAuthnPasskeyLogin(ctx *context.Context) {
|
||||
parsedResponse, err := protocol.ParseCredentialRequestResponse(ctx.Req)
|
||||
if err != nil {
|
||||
// Failed authentication attempt.
|
||||
log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err)
|
||||
log.Info("Failed authentication attempt from %s: %v", ctx.RemoteAddr(), err)
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
@@ -147,12 +148,9 @@ func WebAuthnPasskeyLogin(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Now handle account linking if that's requested
|
||||
if ctx.Session.Get("linkAccount") != nil {
|
||||
if err := linkAccountFromContext(ctx, user); err != nil {
|
||||
ctx.ServerError("LinkAccountFromStore", err)
|
||||
return
|
||||
}
|
||||
if err := completePendingLinks(ctx, user); err != nil {
|
||||
ctx.ServerError("completePendingLinks", err)
|
||||
return
|
||||
}
|
||||
|
||||
remember := false // TODO: implement remember me
|
||||
@@ -186,7 +184,8 @@ func WebAuthnLoginAssertion(ctx *context.Context) {
|
||||
}
|
||||
|
||||
webAuthnUser := wa.NewWebAuthnUser(ctx, user)
|
||||
assertion, sessionData, err := wa.WebAuthn.BeginLogin(webAuthnUser)
|
||||
// "discouraged" would hide credProtect protected credentials
|
||||
assertion, sessionData, err := wa.WebAuthn.BeginLogin(webAuthnUser, webauthn.WithUserVerification(protocol.VerificationPreferred))
|
||||
if err != nil {
|
||||
ctx.ServerError("webauthn.BeginLogin", err)
|
||||
return
|
||||
@@ -261,12 +260,9 @@ func WebAuthnLoginAssertionPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Now handle account linking if that's requested
|
||||
if ctx.Session.Get("linkAccount") != nil {
|
||||
if err := linkAccountFromContext(ctx, user); err != nil {
|
||||
ctx.ServerError("LinkAccountFromStore", err)
|
||||
return
|
||||
}
|
||||
if err := completePendingLinks(ctx, user); err != nil {
|
||||
ctx.ServerError("completePendingLinks", err)
|
||||
return
|
||||
}
|
||||
|
||||
remember := ctx.Session.Get("twofaRemember").(bool)
|
||||
|
||||
@@ -12,14 +12,14 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/assetfs"
|
||||
"code.gitea.io/gitea/modules/avatar"
|
||||
"code.gitea.io/gitea/modules/httpcache"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web/routing"
|
||||
"gitea.dev/modules/assetfs"
|
||||
"gitea.dev/modules/avatar"
|
||||
"gitea.dev/modules/httpcache"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/storage"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web/routing"
|
||||
)
|
||||
|
||||
func avatarStorageHandler(storageSetting *setting.Storage, prefix string, objStore storage.ObjectStorage) http.HandlerFunc {
|
||||
|
||||
@@ -13,16 +13,17 @@ import (
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"code.gitea.io/gitea/models/asymkey"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/badge"
|
||||
"code.gitea.io/gitea/modules/charset"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/indexer/code"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/models/asymkey"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/gituser"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/badge"
|
||||
"gitea.dev/modules/charset"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/indexer/code"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// List all devtest templates, they will be used for e2e tests for the UI components
|
||||
@@ -45,8 +46,8 @@ func List(ctx *context.Context) {
|
||||
|
||||
func FetchActionTest(ctx *context.Context) {
|
||||
_ = ctx.Req.ParseForm()
|
||||
ctx.Flash.Info("fetch-action: " + ctx.Req.Method + " " + ctx.Req.RequestURI + "<br>" +
|
||||
"Form: " + ctx.Req.Form.Encode() + "<br>" +
|
||||
ctx.Flash.Info("fetch action: " + ctx.Req.Method + " " + ctx.Req.RequestURI + "\n" +
|
||||
"Form: " + ctx.Req.Form.Encode() + "\n" +
|
||||
"PostForm: " + ctx.Req.PostForm.Encode(),
|
||||
)
|
||||
time.Sleep(2 * time.Second)
|
||||
@@ -61,8 +62,8 @@ func prepareMockDataBadgeCommitSign(ctx *context.Context) {
|
||||
mockUser := mockUsers[0]
|
||||
commits = append(commits, &asymkey.SignCommit{
|
||||
Verification: &asymkey.CommitVerification{},
|
||||
UserCommit: &user_model.UserCommit{
|
||||
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||
UserCommit: &gituser.UserCommit{
|
||||
GitCommit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||
},
|
||||
})
|
||||
commits = append(commits, &asymkey.SignCommit{
|
||||
@@ -73,9 +74,9 @@ func prepareMockDataBadgeCommitSign(ctx *context.Context) {
|
||||
SigningKey: &asymkey.GPGKey{KeyID: "12345678"},
|
||||
TrustStatus: "trusted",
|
||||
},
|
||||
UserCommit: &user_model.UserCommit{
|
||||
User: mockUser,
|
||||
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||
UserCommit: &gituser.UserCommit{
|
||||
AuthorUser: mockUser,
|
||||
GitCommit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||
},
|
||||
})
|
||||
commits = append(commits, &asymkey.SignCommit{
|
||||
@@ -86,9 +87,9 @@ func prepareMockDataBadgeCommitSign(ctx *context.Context) {
|
||||
SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"},
|
||||
TrustStatus: "untrusted",
|
||||
},
|
||||
UserCommit: &user_model.UserCommit{
|
||||
User: mockUser,
|
||||
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||
UserCommit: &gituser.UserCommit{
|
||||
AuthorUser: mockUser,
|
||||
GitCommit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||
},
|
||||
})
|
||||
commits = append(commits, &asymkey.SignCommit{
|
||||
@@ -99,9 +100,9 @@ func prepareMockDataBadgeCommitSign(ctx *context.Context) {
|
||||
SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"},
|
||||
TrustStatus: "other(unmatch)",
|
||||
},
|
||||
UserCommit: &user_model.UserCommit{
|
||||
User: mockUser,
|
||||
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||
UserCommit: &gituser.UserCommit{
|
||||
AuthorUser: mockUser,
|
||||
GitCommit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||
},
|
||||
})
|
||||
commits = append(commits, &asymkey.SignCommit{
|
||||
@@ -110,9 +111,9 @@ func prepareMockDataBadgeCommitSign(ctx *context.Context) {
|
||||
Reason: "gpg.error",
|
||||
SigningEmail: "test@example.com",
|
||||
},
|
||||
UserCommit: &user_model.UserCommit{
|
||||
User: mockUser,
|
||||
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||
UserCommit: &gituser.UserCommit{
|
||||
AuthorUser: mockUser,
|
||||
GitCommit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -159,6 +160,59 @@ func prepareMockDataBadgeActionsSvg(ctx *context.Context) {
|
||||
ctx.Data["SelectedStyle"] = selectedStyle
|
||||
}
|
||||
|
||||
func prepareMockDataAvatarStack(ctx *context.Context) {
|
||||
/*
|
||||
mockUsers, _ := db.Find[user_model.User](ctx, user_model.SearchUserOptions{ListOptions: db.ListOptions{PageSize: 3}})
|
||||
if len(mockUsers) == 0 {
|
||||
return
|
||||
}
|
||||
u0 := mockUsers[0]
|
||||
u1, u2 := u0, u0
|
||||
if len(mockUsers) >= 2 {
|
||||
u1 = mockUsers[1]
|
||||
}
|
||||
if len(mockUsers) >= 3 {
|
||||
u2 = mockUsers[2]
|
||||
}
|
||||
|
||||
authorSig := func(u *user_model.User) *git.Signature {
|
||||
return &git.Signature{Name: u.Name, Email: u.Email}
|
||||
}
|
||||
coLinked := func(u *user_model.User) *gituser.CommitParticipant {
|
||||
return &gituser.CommitParticipant{GiteaUser: u, GitIdentity: authorSig(u)}
|
||||
}
|
||||
coUnlinked := func(name, email string) *gituser.CommitParticipant {
|
||||
return &gituser.CommitParticipant{GitIdentity: &git.Signature{Name: name, Email: email}}
|
||||
}
|
||||
nUnlinked := func(n int) []*gituser.CommitParticipant {
|
||||
out := make([]*gituser.CommitParticipant, n)
|
||||
for i := range out {
|
||||
out[i] = coUnlinked(fmt.Sprintf("Contributor %d", i+1), fmt.Sprintf("contrib%d@example.com", i+1))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type scenario struct {
|
||||
Label string
|
||||
Data *gituser.AvatarStackData
|
||||
}
|
||||
mk := gituser.BuildAvatarStackData()
|
||||
extSig := &git.Signature{Name: "External Contributor", Email: "external@example.com"}
|
||||
ctx.Data["AvatarStackScenarios"] = []scenario{
|
||||
{Label: "linked author, no co-authors", Data: mk(u0, authorSig(u0), nil)},
|
||||
{Label: "unlinked author, no co-authors", Data: mk(nil, extSig, nil)},
|
||||
{Label: "1 linked co-author", Data: mk(u0, authorSig(u0), []*gituser.CommitParticipant{coLinked(u1)})},
|
||||
{Label: "1 unlinked co-author", Data: mk(u0, authorSig(u0), []*gituser.CommitParticipant{coUnlinked("Bob Smith", "bob@example.com")})},
|
||||
{Label: "2 co-authors (3 people), u1 author", Data: mk(u1, authorSig(u1), []*gituser.CommitParticipant{coLinked(u0), coUnlinked("Bob Smith", "bob@example.com")})},
|
||||
{Label: "3 co-authors mixed (4 people)", Data: mk(u0, authorSig(u0), []*gituser.CommitParticipant{coLinked(u1), coLinked(u2), coUnlinked("Bob Smith", "bob@example.com")})},
|
||||
{Label: "9 co-authors (max visible, no overflow), u2 author", Data: mk(u2, authorSig(u2), nUnlinked(9))},
|
||||
{Label: "10 co-authors (overflow +1)", Data: mk(u0, authorSig(u0), nUnlinked(10))},
|
||||
{Label: "15 co-authors (overflow +6), unlinked author", Data: mk(nil, extSig, nUnlinked(15))},
|
||||
{Label: "30 co-authors (overflow +21)", Data: mk(u0, authorSig(u0), nUnlinked(30))},
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
func prepareMockDataRelativeTime(ctx *context.Context) {
|
||||
now := time.Now()
|
||||
ctx.Data["TimeNow"] = now
|
||||
@@ -192,11 +246,33 @@ func prepareMockData(ctx *context.Context) {
|
||||
prepareMockDataBadgeActionsSvg(ctx)
|
||||
case "/devtest/relative-time":
|
||||
prepareMockDataRelativeTime(ctx)
|
||||
case "/devtest/toast-and-message":
|
||||
prepareMockDataToastAndMessage(ctx)
|
||||
case "/devtest/unicode-escape":
|
||||
prepareMockDataUnicodeEscape(ctx)
|
||||
case "/devtest/avatar-stack":
|
||||
prepareMockDataAvatarStack(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func prepareMockDataToastAndMessage(ctx *context.Context) {
|
||||
msgWithDetails, _ := ctx.RenderToHTML("base/alert_details", map[string]any{
|
||||
"Message": "message with details <script>escape xss</script>",
|
||||
"Summary": "summary with details",
|
||||
"Details": "details line 1\n details line 2\n details line 3",
|
||||
})
|
||||
msgWithSummary, _ := ctx.RenderToHTML("base/alert_details", map[string]any{
|
||||
"Message": "message with summary <script>escape xss</script>",
|
||||
"Summary": "summary only",
|
||||
})
|
||||
|
||||
ctx.Flash.ErrorMsg = string(msgWithDetails)
|
||||
ctx.Flash.WarningMsg = string(msgWithSummary)
|
||||
ctx.Flash.InfoMsg = "a long message with line break\nthe second line <script>removed xss</script>"
|
||||
ctx.Flash.SuccessMsg = "single line message <script>removed xss</script>"
|
||||
ctx.Data["Flash"] = ctx.Flash
|
||||
}
|
||||
|
||||
func prepareMockDataUnicodeEscape(ctx *context.Context) {
|
||||
content := "// demo code\n"
|
||||
content += "if accessLevel != \"user\u202E \u2066// Check if admin (invisible char)\u2069 \u2066\" { }\n"
|
||||
@@ -221,10 +297,9 @@ func prepareMockDataUnicodeEscape(ctx *context.Context) {
|
||||
|
||||
func TmplCommon(ctx *context.Context) {
|
||||
prepareMockData(ctx)
|
||||
if ctx.Req.Method == http.MethodPost {
|
||||
_ = ctx.Req.ParseForm()
|
||||
ctx.Flash.Info("form: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"<br>"+
|
||||
"Form: "+ctx.Req.Form.Encode()+"<br>"+
|
||||
if ctx.Req.Method == http.MethodPost && ctx.FormBool("mock_response_delay") {
|
||||
ctx.Flash.Info("form submit: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"\n"+
|
||||
"Form: "+ctx.Req.Form.Encode()+"\n"+
|
||||
"PostForm: "+ctx.Req.PostForm.Encode(),
|
||||
true,
|
||||
)
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/mailer"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/mailer"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
func MailPreviewRender(ctx *context.Context) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package devtest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
mathRand "math/rand/v2"
|
||||
"net/http"
|
||||
"slices"
|
||||
@@ -11,12 +12,15 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/web/repo/actions"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
actions_model "gitea.dev/models/actions"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/web/repo/actions"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
type generateMockStepsLogOptions struct {
|
||||
@@ -63,73 +67,270 @@ func generateMockStepsLog(logCur actions.LogCursor, opts generateMockStepsLogOpt
|
||||
}
|
||||
|
||||
func MockActionsView(ctx *context.Context) {
|
||||
ctx.Data["RunID"] = ctx.PathParamInt64("run")
|
||||
if runID := ctx.PathParamInt64("run"); runID == 0 {
|
||||
ctx.Redirect("/repo-action-view/runs/10")
|
||||
return
|
||||
}
|
||||
ctx.Data["JobID"] = ctx.PathParamInt64("job")
|
||||
ctx.Data["ActionsViewURL"] = ctx.Req.URL.Path
|
||||
ctx.HTML(http.StatusOK, "devtest/repo-action-view")
|
||||
}
|
||||
|
||||
func MockActionsRunsJobs(ctx *context.Context) {
|
||||
runID := ctx.PathParamInt64("run")
|
||||
attemptID := ctx.PathParamInt64("attempt")
|
||||
|
||||
alignTime := func(v, unit int64) int64 {
|
||||
return (v + unit) / unit * unit
|
||||
}
|
||||
resp := &actions.ViewResponse{}
|
||||
resp.State.Run.RepoID = 12345
|
||||
resp.State.Run.Index = runID
|
||||
resp.State.Run.TitleHTML = `mock run title <a href="/">link</a>`
|
||||
resp.State.Run.Link = setting.AppSubURL + "/devtest/repo-action-view/runs/" + strconv.FormatInt(runID, 10)
|
||||
resp.State.Run.Status = actions_model.StatusRunning.String()
|
||||
resp.State.Run.CanCancel = runID == 10
|
||||
resp.State.Run.CanApprove = runID == 20
|
||||
resp.State.Run.CanRerun = runID == 30
|
||||
resp.State.Run.CanRerunFailed = runID == 30
|
||||
resp.State.Run.CanDeleteArtifact = true
|
||||
resp.State.Run.WorkflowID = "workflow-id"
|
||||
resp.State.Run.WorkflowLink = "./workflow-link"
|
||||
resp.State.Run.Duration = "1h 23m 45s"
|
||||
resp.State.Run.TriggeredAt = time.Now().Add(-time.Hour).Unix()
|
||||
resp.State.Run.WorkflowID = "workflow-id.yml"
|
||||
resp.State.Run.TriggerEvent = "push"
|
||||
renderUtils := templates.NewRenderUtils(ctx)
|
||||
user2, _ := user_model.GetUserByID(ctx, 2)
|
||||
if user2 == nil {
|
||||
user2 = &user_model.User{Name: "user2"}
|
||||
}
|
||||
user3, _ := user_model.GetUserByID(ctx, 3)
|
||||
if user3 == nil {
|
||||
user3 = &user_model.User{Name: "user3"}
|
||||
}
|
||||
resp.State.Run.Commit = actions.ViewCommit{
|
||||
ShortSha: "ccccdddd",
|
||||
Link: "./commit-link",
|
||||
Pusher: actions.ViewUser{
|
||||
DisplayName: "pusher user",
|
||||
Link: "./pusher-link",
|
||||
DisplayName: user2.GetDisplayName(),
|
||||
Link: user2.HomeLink(),
|
||||
AvatarLink: user2.AvatarLinkWithSize(ctx, 16),
|
||||
},
|
||||
Branch: actions.ViewBranch{
|
||||
Name: "commit-branch",
|
||||
Name: "user2:commit-branch",
|
||||
Link: "./branch-link",
|
||||
IsDeleted: false,
|
||||
},
|
||||
}
|
||||
resp.State.Run.PullRequest = &actions.ViewPullRequest{
|
||||
Index: "#37658",
|
||||
Link: "./pull/37658",
|
||||
}
|
||||
now := time.Now()
|
||||
currentAttemptNum := int64(1)
|
||||
if attemptID > 0 {
|
||||
currentAttemptNum = attemptID
|
||||
}
|
||||
attempts := []*actions_model.ActionRunAttempt{{
|
||||
Attempt: 1,
|
||||
Status: actions_model.StatusSuccess,
|
||||
Created: timeutil.TimeStamp(now.Add(-time.Hour).Unix()),
|
||||
TriggerUserID: 2,
|
||||
TriggerUser: user2,
|
||||
}}
|
||||
if runID == 10 {
|
||||
attempts = []*actions_model.ActionRunAttempt{
|
||||
{
|
||||
Attempt: 3,
|
||||
Status: actions_model.StatusSuccess,
|
||||
Created: timeutil.TimeStamp(alignTime(now.Add(-time.Hour).Unix(), 3600)),
|
||||
TriggerUserID: 2,
|
||||
TriggerUser: user2,
|
||||
},
|
||||
{
|
||||
Attempt: 2,
|
||||
Status: actions_model.StatusFailure,
|
||||
Created: timeutil.TimeStamp(alignTime(now.Add(-2*time.Hour).Unix(), 3600)),
|
||||
TriggerUserID: 1,
|
||||
TriggerUser: user3,
|
||||
},
|
||||
{
|
||||
Attempt: 1,
|
||||
Status: actions_model.StatusSuccess,
|
||||
Created: timeutil.TimeStamp(alignTime(now.Add(-3*time.Hour).Unix(), 3600)),
|
||||
TriggerUserID: 2,
|
||||
TriggerUser: user2,
|
||||
},
|
||||
}
|
||||
if attemptID == 0 {
|
||||
currentAttemptNum = 3
|
||||
}
|
||||
}
|
||||
|
||||
latestAttempt := attempts[0]
|
||||
resp.State.Run.RunAttempt = currentAttemptNum
|
||||
resp.State.Run.Done = latestAttempt.Status.IsDone()
|
||||
resp.State.Run.Status = latestAttempt.Status.String()
|
||||
resp.State.Run.Duration = "1h 23m 45s"
|
||||
resp.State.Run.TriggeredAt = latestAttempt.Created.AsTime().Unix()
|
||||
resp.State.Run.ViewLink = resp.State.Run.Link
|
||||
for _, attempt := range attempts {
|
||||
link := resp.State.Run.Link
|
||||
if attempt.Attempt != latestAttempt.Attempt {
|
||||
link = fmt.Sprintf("%s/attempts/%d", resp.State.Run.Link, attempt.Attempt)
|
||||
}
|
||||
current := attempt.Attempt == currentAttemptNum
|
||||
if current {
|
||||
resp.State.Run.Status = attempt.Status.String()
|
||||
resp.State.Run.Done = attempt.Status.IsDone()
|
||||
resp.State.Run.TriggeredAt = attempt.Created.AsTime().Unix()
|
||||
if attempt.Attempt != latestAttempt.Attempt {
|
||||
resp.State.Run.ViewLink = link
|
||||
}
|
||||
}
|
||||
resp.State.Run.Attempts = append(resp.State.Run.Attempts, &actions.ViewRunAttempt{
|
||||
Attempt: attempt.Attempt,
|
||||
Status: attempt.Status.String(),
|
||||
Done: attempt.Status.IsDone(),
|
||||
Link: link,
|
||||
Current: current,
|
||||
Latest: attempt.Attempt == latestAttempt.Attempt,
|
||||
TriggeredAt: attempt.Created.AsTime().Unix(),
|
||||
TriggerUserName: attempt.TriggerUser.GetDisplayName(),
|
||||
TriggerUserLink: attempt.TriggerUser.HomeLink(),
|
||||
TriggerUserAvatar: attempt.TriggerUser.AvatarLinkWithSize(ctx, 16),
|
||||
})
|
||||
}
|
||||
isLatestAttempt := currentAttemptNum == latestAttempt.Attempt
|
||||
resp.State.Run.CanCancel = runID == 10 && isLatestAttempt
|
||||
resp.State.Run.CanApprove = runID == 20 && isLatestAttempt
|
||||
resp.State.Run.CanRerun = runID == 30 && isLatestAttempt
|
||||
resp.State.Run.CanRerunFailed = runID == 30 && isLatestAttempt
|
||||
|
||||
// Mock job summaries so the devtest page can preview the Summary panel rendering.
|
||||
// Only some runs have summaries, so the page also exercises the "no summary" state.
|
||||
if runID == 10 || runID == 20 {
|
||||
resp.State.Run.JobSummaries = []*actions.ViewJobSummary{
|
||||
{
|
||||
JobID: runID * 10,
|
||||
JobName: "job 100 (testsubname)",
|
||||
SummaryHTML: renderUtils.MarkdownToHtml("### Devtest job summary\n\n- Markdown rendering\n- Links: [example](https://example.com)\n\n```sh\necho hello\n```\n"),
|
||||
},
|
||||
{
|
||||
JobID: runID*10 + 2,
|
||||
JobName: "ULTRA LOOOOOOOOOOOONG job name 102 that exceeds the limit",
|
||||
SummaryHTML: renderUtils.MarkdownToHtml("### Another summary\n\nThis demonstrates multiple job summaries in one run.\n\n- Item A\n- Item B\n"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
|
||||
Name: "artifact-a",
|
||||
Size: 100 * 1024,
|
||||
Status: "expired",
|
||||
Name: "artifact-a",
|
||||
Size: 100 * 1024,
|
||||
Status: "expired",
|
||||
ExpiresUnix: alignTime(time.Now().Add(-24*time.Hour).Unix(), 3600),
|
||||
})
|
||||
resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
|
||||
Name: "artifact-b",
|
||||
Size: 1024 * 1024,
|
||||
Status: "completed",
|
||||
Name: "artifact-b",
|
||||
Size: 1024 * 1024,
|
||||
Status: "completed",
|
||||
ExpiresUnix: alignTime(time.Now().Add(24*time.Hour).Unix(), 3600),
|
||||
})
|
||||
resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
|
||||
Name: "artifact-very-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
|
||||
Size: 100 * 1024,
|
||||
Status: "expired",
|
||||
Name: "artifact-very-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
|
||||
Size: 100 * 1024,
|
||||
Status: "expired",
|
||||
ExpiresUnix: alignTime(time.Now().Add(-24*time.Hour).Unix(), 3600),
|
||||
})
|
||||
resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{
|
||||
Name: "artifact-really-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
|
||||
Size: 1024 * 1024,
|
||||
Status: "completed",
|
||||
Name: "artifact-really-loooooooooooooooooooooooooooooooooooooooooooooooooooooooong",
|
||||
Size: 1024 * 1024,
|
||||
Status: "completed",
|
||||
ExpiresUnix: 0,
|
||||
})
|
||||
|
||||
jobLink := func(jobID int64) string {
|
||||
return fmt.Sprintf("%s/jobs/%d", resp.State.Run.Link, jobID)
|
||||
}
|
||||
|
||||
// Keep devtest mock runs minimal: use run 10 as a "complex graph" repro.
|
||||
// This combines long durations, parallel roots, and a multi-dependency downstream job
|
||||
// to validate the workflow graph rendering.
|
||||
if runID == 10 {
|
||||
resp.State.Run.WorkflowID = "workflow-devtest-complex"
|
||||
resp.State.Run.Duration = "7h 12m 34s"
|
||||
|
||||
type mj struct {
|
||||
jobID string
|
||||
name string
|
||||
status actions_model.Status
|
||||
duration string
|
||||
needs []string
|
||||
}
|
||||
mockJobs := []mj{
|
||||
{jobID: "job-100", name: "job-100", status: actions_model.StatusSuccess, duration: "3s", needs: nil},
|
||||
{jobID: "job-101", name: "job-101", status: actions_model.StatusSuccess, duration: "3s", needs: []string{"job-100"}},
|
||||
{jobID: "job-102", name: "job-102", status: actions_model.StatusSuccess, duration: "4s", needs: []string{"job-100", "job-101"}},
|
||||
{jobID: "job-103", name: "job-103", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"job-100"}},
|
||||
|
||||
{jobID: "prep-jdk", name: "prep-jdk", status: actions_model.StatusSuccess, duration: "3s", needs: nil},
|
||||
{jobID: "code-analysis", name: "code-analysis", status: actions_model.StatusSuccess, duration: "3s", needs: nil},
|
||||
|
||||
// Matrix expansion: the legs share a single JobID, which is what the frontend groups rows on
|
||||
{jobID: "matrix-e2e", name: "matrix-e2e (1, chromium)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "matrix-e2e", name: "matrix-e2e (1, firefox)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "matrix-e2e", name: "matrix-e2e (2, chromium)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "matrix-e2e", name: "matrix-e2e (3, chromium)", status: actions_model.StatusSuccess, duration: "4s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "matrix-e2e", name: "matrix-e2e (3, firefox)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "matrix-e2e", name: "matrix-e2e (99, webkit)", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
|
||||
// Matrix legs whose `name:` interpolates matrix values, so no " (...)" suffix is derived
|
||||
{jobID: "e2e-browsers", name: "E2E on chromium", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "e2e-browsers", name: "E2E on firefox", status: actions_model.StatusSuccess, duration: "3s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "e2e-browsers", name: "E2E on webkit", status: actions_model.StatusSuccess, duration: "2s", needs: []string{"prep-jdk"}},
|
||||
|
||||
{jobID: "unit-test", name: "unit-test", status: actions_model.StatusSuccess, duration: "3s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "arch-test", name: "arch-test", status: actions_model.StatusSuccess, duration: "3s", needs: []string{"prep-jdk"}},
|
||||
{jobID: "integration-test", name: "integration-test", status: actions_model.StatusSuccess, duration: "4s", needs: []string{"prep-jdk"}},
|
||||
|
||||
{jobID: "build-image", name: "build-image with a very long name that does not fit into the sidebar", status: actions_model.StatusSuccess, duration: "3s", needs: []string{
|
||||
"unit-test",
|
||||
"arch-test",
|
||||
"integration-test",
|
||||
"code-analysis",
|
||||
"matrix-e2e",
|
||||
"e2e-browsers",
|
||||
}},
|
||||
|
||||
// Separate jobs that only look like matrix legs, so they must stay separate nodes
|
||||
{jobID: "deploy-staging", name: "Deploy (staging)", status: actions_model.StatusSuccess, duration: "5s", needs: []string{"build-image"}},
|
||||
{jobID: "deploy-prod", name: "Deploy (prod)", status: actions_model.StatusSuccess, duration: "6s", needs: []string{"deploy-staging"}},
|
||||
}
|
||||
|
||||
resp.State.Run.Jobs = nil
|
||||
for i, j := range mockJobs {
|
||||
id := runID*1000 + int64(i)
|
||||
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
|
||||
ID: id,
|
||||
Link: jobLink(id),
|
||||
JobID: j.jobID,
|
||||
Name: j.name,
|
||||
Status: j.status.String(),
|
||||
CanRerun: j.jobID == "job-100",
|
||||
Duration: j.duration,
|
||||
Needs: j.needs,
|
||||
})
|
||||
}
|
||||
|
||||
fillViewRunResponseCurrentJob(ctx, resp)
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
return
|
||||
}
|
||||
|
||||
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
|
||||
ID: runID * 10,
|
||||
Link: jobLink(runID * 10),
|
||||
JobID: "job-100",
|
||||
Name: "job 100",
|
||||
Name: "job 100 (testsubname)",
|
||||
Status: actions_model.StatusRunning.String(),
|
||||
CanRerun: true,
|
||||
Duration: "1h",
|
||||
Duration: "1h23m45s",
|
||||
})
|
||||
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
|
||||
ID: runID*10 + 1,
|
||||
Link: jobLink(runID*10 + 1),
|
||||
JobID: "job-101",
|
||||
Name: "job 101",
|
||||
Status: actions_model.StatusWaiting.String(),
|
||||
@@ -139,15 +340,17 @@ func MockActionsRunsJobs(ctx *context.Context) {
|
||||
})
|
||||
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
|
||||
ID: runID*10 + 2,
|
||||
Link: jobLink(runID*10 + 2),
|
||||
JobID: "job-102",
|
||||
Name: "ULTRA LOOOOOOOOOOOONG job name 102 that exceeds the limit",
|
||||
Status: actions_model.StatusFailure.String(),
|
||||
CanRerun: false,
|
||||
Duration: "3h",
|
||||
Duration: "3h35m10s",
|
||||
Needs: []string{"job-100", "job-101"},
|
||||
})
|
||||
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
|
||||
ID: runID*10 + 3,
|
||||
Link: jobLink(runID*10 + 3),
|
||||
JobID: "job-103",
|
||||
Name: "job 103",
|
||||
Status: actions_model.StatusCancelled.String(),
|
||||
@@ -159,8 +362,10 @@ func MockActionsRunsJobs(ctx *context.Context) {
|
||||
// add more jobs to a run for UI testing
|
||||
if resp.State.Run.CanCancel {
|
||||
for i := range 10 {
|
||||
jobID := runID*1000 + int64(i)
|
||||
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &actions.ViewJob{
|
||||
ID: runID*1000 + int64(i),
|
||||
ID: jobID,
|
||||
Link: jobLink(jobID),
|
||||
JobID: "job-dup-test-" + strconv.Itoa(i),
|
||||
Name: "job dup test " + strconv.Itoa(i),
|
||||
Status: actions_model.StatusSuccess.String(),
|
||||
@@ -171,6 +376,139 @@ func MockActionsRunsJobs(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
if runID == 40 {
|
||||
// Reusable workflow caller demo: same-repo caller (with a nested same-repo caller inside),
|
||||
// alongside a flat cross-repo caller.
|
||||
// Layout:
|
||||
// prepare (regular, top-level)
|
||||
// local_caller (caller, same-repo, expanded)
|
||||
// ├ lib_step (regular)
|
||||
// └ inner_caller (caller, same-repo nested, expanded)
|
||||
// └ deep_job (regular)
|
||||
// cross_caller (caller, cross-repo, expanded)
|
||||
// └ external_job (regular)
|
||||
// build (linux|windows|macos) (regular matrix; graph folds into one "build" node)
|
||||
// build-call (linux|windows|macos) (caller matrix, each calls build.yml; folds into one "build-call" node like "build")
|
||||
// final (regular, needs local_caller + cross_caller)
|
||||
const (
|
||||
prepareID = int64(400)
|
||||
localCallerID = int64(401)
|
||||
libStepID = int64(402)
|
||||
innerCallerID = int64(403)
|
||||
deepJobID = int64(404)
|
||||
crossCallerID = int64(405)
|
||||
externalJobID = int64(406)
|
||||
finalID = int64(407)
|
||||
|
||||
// Regular matrix set – the graph already folds these into a single "build" node.
|
||||
buildLinuxID = int64(410)
|
||||
buildWindowsID = int64(411)
|
||||
buildMacosID = int64(412)
|
||||
|
||||
// Caller matrix set – each matrix leg calls the same reusable workflow. #38466: like the
|
||||
// regular "build" matrix above, these fold into one "build-call" node. Matrix legs share a
|
||||
// single JobID, so the legs below use JobID "build-call" and differ only by their name suffix.
|
||||
buildCallLinuxID = int64(420)
|
||||
buildCallWindowsID = int64(421)
|
||||
buildCallMacosID = int64(422)
|
||||
buildCallLinuxJobID = int64(423)
|
||||
buildCallWinJobID = int64(424)
|
||||
buildCallMacJobID = int64(425)
|
||||
)
|
||||
|
||||
resp.State.Run.Jobs = []*actions.ViewJob{
|
||||
{
|
||||
ID: prepareID, Link: jobLink(prepareID), JobID: "prepare", Name: "prepare",
|
||||
Status: actions_model.StatusSuccess.String(), Duration: "30s",
|
||||
},
|
||||
{
|
||||
ID: localCallerID, Link: jobLink(localCallerID), JobID: "local_caller", Name: "local caller",
|
||||
Status: actions_model.StatusRunning.String(), Duration: "5m",
|
||||
Needs: []string{"prepare"},
|
||||
IsReusableCaller: true, CallUses: "./.gitea/workflows/lib.yml",
|
||||
},
|
||||
{
|
||||
ID: libStepID, Link: jobLink(libStepID), JobID: "lib_step", Name: "lib step",
|
||||
Status: actions_model.StatusSuccess.String(), Duration: "1m",
|
||||
ParentJobID: localCallerID,
|
||||
},
|
||||
{
|
||||
ID: innerCallerID, Link: jobLink(innerCallerID), JobID: "inner_caller", Name: "inner caller (nested)",
|
||||
Status: actions_model.StatusRunning.String(), Duration: "4m",
|
||||
ParentJobID: localCallerID,
|
||||
IsReusableCaller: true, CallUses: "./.gitea/workflows/inner.yml",
|
||||
},
|
||||
{
|
||||
ID: deepJobID, Link: jobLink(deepJobID), JobID: "deep_job", Name: "deep job",
|
||||
Status: actions_model.StatusRunning.String(), Duration: "2m",
|
||||
ParentJobID: innerCallerID,
|
||||
},
|
||||
{
|
||||
ID: crossCallerID, Link: jobLink(crossCallerID), JobID: "cross_caller", Name: "cross-repo caller",
|
||||
Status: actions_model.StatusWaiting.String(), Duration: "0s",
|
||||
Needs: []string{"prepare"},
|
||||
IsReusableCaller: true, CallUses: "user2/lib-repo/.gitea/workflows/external.yml@main",
|
||||
},
|
||||
{
|
||||
ID: externalJobID, Link: jobLink(externalJobID), JobID: "external_job", Name: "external job",
|
||||
Status: actions_model.StatusWaiting.String(), Duration: "0s",
|
||||
ParentJobID: crossCallerID,
|
||||
},
|
||||
|
||||
// Regular matrix "build" – these fold into one matrix node in the graph. The matrix legs
|
||||
// share a single JobID ("build"); the " (variant)" name suffix distinguishes the legs.
|
||||
{
|
||||
ID: buildLinuxID, Link: jobLink(buildLinuxID), JobID: "build", Name: "build (linux)",
|
||||
Status: actions_model.StatusSuccess.String(), Duration: "1m", Needs: []string{"prepare"},
|
||||
},
|
||||
{
|
||||
ID: buildWindowsID, Link: jobLink(buildWindowsID), JobID: "build", Name: "build (windows)",
|
||||
Status: actions_model.StatusSuccess.String(), Duration: "2m", Needs: []string{"prepare"},
|
||||
},
|
||||
{
|
||||
ID: buildMacosID, Link: jobLink(buildMacosID), JobID: "build", Name: "build (macos)",
|
||||
Status: actions_model.StatusSuccess.String(), Duration: "90s", Needs: []string{"prepare"},
|
||||
},
|
||||
|
||||
// Caller matrix "build-call" – each leg calls the same reusable workflow. #38466: like the
|
||||
// regular "build" matrix above, these fold into one node. The matrix legs share a single
|
||||
// JobID ("build-call"); the " (variant)" name suffix distinguishes the legs.
|
||||
{
|
||||
ID: buildCallLinuxID, Link: jobLink(buildCallLinuxID), JobID: "build-call", Name: "build-call (linux)",
|
||||
Status: actions_model.StatusSuccess.String(), Duration: "1m", Needs: []string{"prepare"},
|
||||
IsReusableCaller: true, CallUses: "./.gitea/workflows/build.yml",
|
||||
},
|
||||
{
|
||||
ID: buildCallLinuxJobID, Link: jobLink(buildCallLinuxJobID), JobID: "bc_linux_build", Name: "build",
|
||||
Status: actions_model.StatusSuccess.String(), Duration: "1m", ParentJobID: buildCallLinuxID,
|
||||
},
|
||||
{
|
||||
ID: buildCallWindowsID, Link: jobLink(buildCallWindowsID), JobID: "build-call", Name: "build-call (windows)",
|
||||
Status: actions_model.StatusSuccess.String(), Duration: "2m", Needs: []string{"prepare"},
|
||||
IsReusableCaller: true, CallUses: "./.gitea/workflows/build.yml",
|
||||
},
|
||||
{
|
||||
ID: buildCallWinJobID, Link: jobLink(buildCallWinJobID), JobID: "bc_windows_build", Name: "build",
|
||||
Status: actions_model.StatusSuccess.String(), Duration: "2m", ParentJobID: buildCallWindowsID,
|
||||
},
|
||||
{
|
||||
ID: buildCallMacosID, Link: jobLink(buildCallMacosID), JobID: "build-call", Name: "build-call (macos)",
|
||||
Status: actions_model.StatusSuccess.String(), Duration: "90s", Needs: []string{"prepare"},
|
||||
IsReusableCaller: true, CallUses: "./.gitea/workflows/build.yml",
|
||||
},
|
||||
{
|
||||
ID: buildCallMacJobID, Link: jobLink(buildCallMacJobID), JobID: "bc_macos_build", Name: "build",
|
||||
Status: actions_model.StatusSuccess.String(), Duration: "90s", ParentJobID: buildCallMacosID,
|
||||
},
|
||||
|
||||
{
|
||||
ID: finalID, Link: jobLink(finalID), JobID: "final", Name: "final",
|
||||
Status: actions_model.StatusBlocked.String(), Duration: "0s",
|
||||
Needs: []string{"local_caller", "cross_caller"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fillViewRunResponseCurrentJob(ctx, resp)
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
@@ -181,6 +519,14 @@ func fillViewRunResponseCurrentJob(ctx *context.Context, resp *actions.ViewRespo
|
||||
return
|
||||
}
|
||||
|
||||
for _, job := range resp.State.Run.Jobs {
|
||||
if job.ID == jobID {
|
||||
resp.State.CurrentJob.Title = job.Name
|
||||
resp.State.CurrentJob.Detail = job.Status
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
req := web.GetForm(ctx).(*actions.ViewRequest)
|
||||
var mockLogOptions []generateMockStepsLogOptions
|
||||
resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &actions.ViewJobStep{
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/eventsource"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/routers/web/auth"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/modules/eventsource"
|
||||
"gitea.dev/modules/graceful"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/routers/web/auth"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// Events listens for events
|
||||
|
||||
@@ -7,13 +7,13 @@ import (
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
code_indexer "code.gitea.io/gitea/modules/indexer/code"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/routers/common"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
code_indexer "gitea.dev/modules/indexer/code"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/routers/common"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
package explore
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// Organizations render explore organizations page
|
||||
@@ -38,17 +38,14 @@ func Organizations(ctx *context.Context) {
|
||||
"alphabetically",
|
||||
"reversealphabetically",
|
||||
)
|
||||
sortOrder := ctx.FormString("sort")
|
||||
if sortOrder == "" {
|
||||
sortOrder = util.Iif(supportedSortOrders.Contains(setting.UI.ExploreDefaultSort), setting.UI.ExploreDefaultSort, "newest")
|
||||
ctx.SetFormString("sort", sortOrder)
|
||||
}
|
||||
|
||||
sortOrderDefault := util.Iif(supportedSortOrders.Contains(setting.UI.ExploreDefaultSort), setting.UI.ExploreDefaultSort, "newest")
|
||||
sortOrder := ctx.FormString("sort", sortOrderDefault)
|
||||
RenderUserSearch(ctx, user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
Types: []user_model.UserType{user_model.UserTypeOrganization},
|
||||
ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},
|
||||
Visible: visibleTypes,
|
||||
OrderBy: db.SearchOrderBy(sortOrder),
|
||||
|
||||
SupportedSortOrders: supportedSortOrders,
|
||||
}, tplExploreUsers)
|
||||
|
||||
@@ -6,13 +6,13 @@ package explore
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/sitemap"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/sitemap"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -6,11 +6,11 @@ package explore
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
)
|
||||
|
||||
// TopicSearch search for creating topic
|
||||
|
||||
@@ -7,17 +7,17 @@ import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/sitemap"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/models/db"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/sitemap"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -55,11 +55,7 @@ func RenderUserSearch(ctx *context.Context, opts user_model.SearchUserOptions, t
|
||||
)
|
||||
|
||||
// we can not set orderBy to `models.SearchOrderByXxx`, because there may be a JOIN in the statement, different tables may have the same name columns
|
||||
|
||||
sortOrder := ctx.FormString("sort")
|
||||
if sortOrder == "" {
|
||||
sortOrder = setting.UI.ExploreDefaultSort
|
||||
}
|
||||
sortOrder := util.IfZero(string(opts.OrderBy), ctx.FormString("sort", setting.UI.ExploreDefaultSort))
|
||||
ctx.Data["SortType"] = sortOrder
|
||||
|
||||
switch sortOrder {
|
||||
@@ -145,18 +141,15 @@ func Users(ctx *context.Context) {
|
||||
"alphabetically",
|
||||
"reversealphabetically",
|
||||
)
|
||||
sortOrder := ctx.FormString("sort")
|
||||
if sortOrder == "" {
|
||||
sortOrder = util.Iif(supportedSortOrders.Contains(setting.UI.ExploreDefaultSort), setting.UI.ExploreDefaultSort, "newest")
|
||||
ctx.SetFormString("sort", sortOrder)
|
||||
}
|
||||
|
||||
sortOrderDefault := util.Iif(supportedSortOrders.Contains(setting.UI.ExploreDefaultSort), setting.UI.ExploreDefaultSort, "newest")
|
||||
sortOrder := ctx.FormString("sort", sortOrderDefault)
|
||||
RenderUserSearch(ctx, user_model.SearchUserOptions{
|
||||
Actor: ctx.Doer,
|
||||
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
||||
ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum},
|
||||
IsActive: optional.Some(true),
|
||||
Visible: []structs.VisibleType{structs.VisibleTypePublic, structs.VisibleTypeLimited, structs.VisibleTypePrivate},
|
||||
OrderBy: db.SearchOrderBy(sortOrder),
|
||||
|
||||
SupportedSortOrders: supportedSortOrders,
|
||||
}, tplExploreUsers)
|
||||
|
||||
@@ -4,18 +4,20 @@
|
||||
package feed
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/models/repo"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/services/context"
|
||||
|
||||
"github.com/gorilla/feeds"
|
||||
)
|
||||
|
||||
// ShowBranchFeed shows tags and/or releases on the repo as RSS / Atom feed
|
||||
func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType string) {
|
||||
if !checkRepoFeedTokenScope(ctx) {
|
||||
return
|
||||
}
|
||||
var commits []*git.Commit
|
||||
var err error
|
||||
if ctx.Repo.Commit != nil {
|
||||
@@ -39,14 +41,14 @@ func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType stri
|
||||
for _, commit := range commits {
|
||||
feed.Items = append(feed.Items, &feeds.Item{
|
||||
Id: commit.ID.String(),
|
||||
Title: strings.TrimSpace(strings.Split(commit.Message(), "\n")[0]),
|
||||
Title: commit.MessageTitle(),
|
||||
Link: &feeds.Link{Href: repo.HTMLURL() + "/commit/" + commit.ID.String()},
|
||||
Author: &feeds.Author{
|
||||
Name: commit.Author.Name,
|
||||
Email: commit.Author.Email,
|
||||
},
|
||||
Description: commit.Message(),
|
||||
Content: commit.Message(),
|
||||
Description: commit.MessageUTF8(), // TODO: description can be shorten content
|
||||
Content: commit.MessageUTF8(),
|
||||
Created: commit.Committer.When,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,14 +12,15 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/renderhelper"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
activities_model "gitea.dev/models/activities"
|
||||
"gitea.dev/models/renderhelper"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/markup"
|
||||
"gitea.dev/modules/markup/markdown"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
|
||||
"github.com/gorilla/feeds"
|
||||
)
|
||||
@@ -237,7 +238,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
|
||||
}
|
||||
}
|
||||
if len(content) == 0 {
|
||||
content = templates.SanitizeHTML(desc)
|
||||
content = markup.Sanitize(desc)
|
||||
}
|
||||
|
||||
items = append(items, &feeds.Item{
|
||||
|
||||
@@ -4,24 +4,26 @@
|
||||
package feed
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/models/repo"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
|
||||
"github.com/gorilla/feeds"
|
||||
)
|
||||
|
||||
// ShowFileFeed shows tags and/or releases on the repo as RSS / Atom feed
|
||||
func ShowFileFeed(ctx *context.Context, repo *repo.Repository, formatType string) {
|
||||
if !checkRepoFeedTokenScope(ctx) {
|
||||
return
|
||||
}
|
||||
fileName := ctx.Repo.TreePath
|
||||
if len(fileName) == 0 {
|
||||
return
|
||||
}
|
||||
commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange(
|
||||
commits, _, err := ctx.Repo.GitRepo.CommitsByFileAndRange(
|
||||
git.CommitsByFileAndRangeOptions{
|
||||
Revision: ctx.Repo.RefFullName.ShortName(), // FIXME: legacy code used ShortName
|
||||
File: fileName,
|
||||
@@ -46,14 +48,14 @@ func ShowFileFeed(ctx *context.Context, repo *repo.Repository, formatType string
|
||||
for _, commit := range commits {
|
||||
feed.Items = append(feed.Items, &feeds.Item{
|
||||
Id: commit.ID.String(),
|
||||
Title: strings.TrimSpace(strings.Split(commit.Message(), "\n")[0]),
|
||||
Title: commit.MessageTitle(),
|
||||
Link: &feeds.Link{Href: repo.HTMLURL() + "/commit/" + commit.ID.String()},
|
||||
Author: &feeds.Author{
|
||||
Name: commit.Author.Name,
|
||||
Email: commit.Author.Email,
|
||||
},
|
||||
Description: commit.Message(),
|
||||
Content: commit.Message(),
|
||||
Description: commit.MessageUTF8(), // TODO: description can be shorten content
|
||||
Content: commit.MessageUTF8(),
|
||||
Created: commit.Committer.When,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ package feed
|
||||
import (
|
||||
"time"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/renderhelper"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
feed_service "code.gitea.io/gitea/services/feed"
|
||||
activities_model "gitea.dev/models/activities"
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/models/renderhelper"
|
||||
"gitea.dev/modules/markup/markdown"
|
||||
"gitea.dev/services/context"
|
||||
feed_service "gitea.dev/services/feed"
|
||||
|
||||
"github.com/gorilla/feeds"
|
||||
)
|
||||
@@ -41,6 +41,11 @@ func showUserFeed(ctx *context.Context, formatType string) {
|
||||
includePrivate = isOrgMember
|
||||
}
|
||||
|
||||
// a public-only API token must not surface private activity, even for its own owner
|
||||
if includePrivate && context.TokenIsPublicOnly(ctx) {
|
||||
includePrivate = false
|
||||
}
|
||||
|
||||
actions, _, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{
|
||||
RequestedUser: ctx.ContextUser,
|
||||
Actor: ctx.Doer,
|
||||
|
||||
@@ -5,10 +5,10 @@ package feed_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/routers/web/feed"
|
||||
"code.gitea.io/gitea/services/contexttest"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/routers/web/feed"
|
||||
"gitea.dev/services/contexttest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -6,15 +6,18 @@ package feed
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/services/context"
|
||||
|
||||
"github.com/gorilla/feeds"
|
||||
)
|
||||
|
||||
// shows tags and/or releases on the repo as RSS / Atom feed
|
||||
func ShowReleaseFeed(ctx *context.Context, repo *repo_model.Repository, isReleasesOnly bool, formatType string) {
|
||||
if !checkRepoFeedTokenScope(ctx) {
|
||||
return
|
||||
}
|
||||
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
|
||||
IncludeTags: !isReleasesOnly,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
|
||||
@@ -4,9 +4,18 @@
|
||||
package feed
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/services/context"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// checkRepoFeedTokenScope ensures an API token has repository read scope before a
|
||||
// feed serves private repository content, mirroring checkDownloadTokenScope for
|
||||
// downloads. Returns false (and writes the response) when the token is denied.
|
||||
func checkRepoFeedTokenScope(ctx *context.Context) bool {
|
||||
context.CheckRepoScopedToken(ctx, ctx.Repo.Repository, auth_model.Read)
|
||||
return !ctx.Written()
|
||||
}
|
||||
|
||||
// RenderBranchFeed render format for branch or file
|
||||
func RenderBranchFeed(ctx *context.Context, feedType string) {
|
||||
if ctx.Repo.TreePath == "" {
|
||||
|
||||
@@ -6,16 +6,19 @@ package feed
|
||||
import (
|
||||
"time"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
feed_service "code.gitea.io/gitea/services/feed"
|
||||
activities_model "gitea.dev/models/activities"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/services/context"
|
||||
feed_service "gitea.dev/services/feed"
|
||||
|
||||
"github.com/gorilla/feeds"
|
||||
)
|
||||
|
||||
// ShowRepoFeed shows user activity on the repo as RSS / Atom feed
|
||||
func ShowRepoFeed(ctx *context.Context, repo *repo_model.Repository, formatType string) {
|
||||
if !checkRepoFeedTokenScope(ctx) {
|
||||
return
|
||||
}
|
||||
actions, _, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{
|
||||
RequestedRepo: repo,
|
||||
Actor: ctx.Doer,
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/web/repo"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/web/repo"
|
||||
)
|
||||
|
||||
func addOwnerRepoGitHTTPRouters(m *web.Router, middlewares ...any) {
|
||||
|
||||
@@ -11,10 +11,14 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
func goGet(ctx *context.Context) {
|
||||
@@ -51,10 +55,8 @@ func goGet(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
branchName := setting.Repository.DefaultBranch
|
||||
|
||||
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName)
|
||||
if err == nil && len(repo.DefaultBranch) > 0 {
|
||||
branchName = repo.DefaultBranch
|
||||
if repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName); err == nil {
|
||||
branchName = goGetDefaultBranch(ctx, repo)
|
||||
}
|
||||
prefix := setting.AppURL + path.Join(url.PathEscape(ownerName), url.PathEscape(repoName), "src", "branch", util.PathEscapeSegments(branchName))
|
||||
|
||||
@@ -91,3 +93,47 @@ func goGet(ctx *context.Context) {
|
||||
ctx.RespHeader().Set("Content-Type", "text/html")
|
||||
_, _ = ctx.Write([]byte(res))
|
||||
}
|
||||
|
||||
// goGetDefaultBranch returns the repository's real default branch only when the caller may genuinely
|
||||
// reach it, otherwise the neutral instance default, so the meta response does not disclose the branch
|
||||
// name (or the repo's existence) to callers who cannot see the repository.
|
||||
func goGetDefaultBranch(ctx *context.Context, repo *repo_model.Repository) string {
|
||||
def := setting.Repository.DefaultBranch
|
||||
if len(repo.DefaultBranch) == 0 {
|
||||
return def
|
||||
}
|
||||
if err := repo.LoadOwner(ctx); err != nil || repo.Owner == nil {
|
||||
return def
|
||||
}
|
||||
// a token that was not granted repository read scope must not learn repository details, even when the
|
||||
// account behind it could read the repo through the web UI
|
||||
if !goGetTokenCanReadRepo(ctx) {
|
||||
return def
|
||||
}
|
||||
// a public-only token may only reach genuinely public resources (a public repo under a public owner)
|
||||
if context.TokenIsPublicOnly(ctx) && (repo.IsPrivate || !repo.Owner.Visibility.IsPublic()) {
|
||||
return def
|
||||
}
|
||||
// the caller must be able to read the code and see the owner: a limited/private owner hides its repos
|
||||
// from anonymous/non-member callers even when the repo itself is public
|
||||
perm, err := access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
|
||||
if err != nil || !perm.CanRead(unit.TypeCode) || !user_model.IsUserVisibleToViewer(ctx, repo.Owner, ctx.Doer) {
|
||||
return def
|
||||
}
|
||||
return repo.DefaultBranch
|
||||
}
|
||||
|
||||
// goGetTokenCanReadRepo reports whether the request may learn repository details. A non-token request
|
||||
// always may; a token request may only when its scope grants repository read, so a PAT that was never
|
||||
// scoped for repositories cannot disclose the branch even if its owner can read the repo.
|
||||
func goGetTokenCanReadRepo(ctx *context.Context) bool {
|
||||
if ctx.Data["IsApiToken"] != true {
|
||||
return true
|
||||
}
|
||||
scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
has, err := scope.HasScope(auth_model.AccessTokenScopeReadRepository)
|
||||
return err == nil && has
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/cache"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
)
|
||||
|
||||
type status string
|
||||
@@ -111,16 +111,10 @@ func checkDatabase(ctx context.Context, checks checks) status {
|
||||
}
|
||||
|
||||
if setting.Database.Type.IsSQLite3() && st.Status == pass {
|
||||
if !setting.EnableSQLite3 {
|
||||
if _, err := os.Stat(setting.Database.Path); err != nil {
|
||||
st.Status = fail
|
||||
st.Time = getCheckTime()
|
||||
log.Error("SQLite3 health check failed with error: %v", "this Gitea binary is built without SQLite3 enabled")
|
||||
} else {
|
||||
if _, err := os.Stat(setting.Database.Path); err != nil {
|
||||
st.Status = fail
|
||||
st.Time = getCheckTime()
|
||||
log.Error("SQLite3 file exists check failed with error: %v", err)
|
||||
}
|
||||
log.Error("SQLite3 file exists check failed with error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,19 +8,19 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"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/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/sitemap"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/routers/web/auth"
|
||||
"code.gitea.io/gitea/routers/web/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/sitemap"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/web/middleware"
|
||||
"gitea.dev/routers/web/auth"
|
||||
"gitea.dev/routers/web/user"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -109,9 +109,3 @@ func HomeSitemap(ctx *context.Context) {
|
||||
log.Error("Failed writing sitemap: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// NotFound render 404 page
|
||||
func NotFound(ctx *context.Context) {
|
||||
ctx.Data["Title"] = "Page Not Found"
|
||||
ctx.NotFound(nil)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"crypto/subtle"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"gitea.dev/modules/setting"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
package misc
|
||||
|
||||
import (
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/common"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/common"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// Markup render markup document to HTML
|
||||
|
||||
@@ -7,16 +7,42 @@ import (
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/httpcache"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/httpcache"
|
||||
"gitea.dev/modules/httplib"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web/middleware"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
func SiteManifest(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/manifest+json")
|
||||
if httpcache.HandleGenericETagPublicCache(req, w, "", &setting.AppStartTime) {
|
||||
return
|
||||
}
|
||||
if req.Method == http.MethodHead {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := req.Context()
|
||||
absoluteAssetURL := strings.TrimSuffix(httplib.MakeAbsoluteURL(ctx, setting.StaticURLPrefix), "/")
|
||||
manifest := map[string]any{
|
||||
"name": setting.AppName,
|
||||
"short_name": setting.AppName,
|
||||
"start_url": httplib.GuessCurrentAppURL(ctx),
|
||||
"icons": []map[string]string{
|
||||
{"src": absoluteAssetURL + "/assets/img/logo.png", "type": "image/png", "sizes": "512x512"},
|
||||
{"src": absoluteAssetURL + "/assets/img/logo.svg", "type": "image/svg+xml", "sizes": "512x512"},
|
||||
},
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(manifest)
|
||||
}
|
||||
|
||||
func SSHInfo(rw http.ResponseWriter, req *http.Request) {
|
||||
if !git.DefaultFeatures().SupportProcReceive {
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
|
||||
@@ -6,7 +6,7 @@ package misc
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
func Swagger(ctx *context.Context) {
|
||||
|
||||
@@ -6,13 +6,13 @@ package misc
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
"code.gitea.io/gitea/services/webtheme"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web/middleware"
|
||||
"gitea.dev/services/context"
|
||||
user_service "gitea.dev/services/user"
|
||||
"gitea.dev/services/webtheme"
|
||||
)
|
||||
|
||||
func WebThemeList(ctx *context.Context) {
|
||||
|
||||
@@ -6,8 +6,8 @@ package web
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
type nodeInfoLinks struct {
|
||||
|
||||
@@ -6,9 +6,9 @@ package org
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/modules/templates"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -34,15 +34,5 @@ func BlockedUsers(ctx *context.Context) {
|
||||
}
|
||||
|
||||
func BlockedUsersPost(ctx *context.Context) {
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
shared_user.BlockedUsersPost(ctx, ctx.ContextUser)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(ctx.ContextUser.OrganisationLink() + "/settings/blocked_users")
|
||||
shared_user.BlockedUsersPost(ctx, ctx.ContextUser, ctx.ContextUser.OrganisationLink()+"/settings/blocked_users")
|
||||
}
|
||||
|
||||
@@ -5,21 +5,20 @@ package org
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/renderhelper"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/models/renderhelper"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/markup/markdown"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const tplOrgHome templates.TplName = "org/home"
|
||||
@@ -98,8 +97,29 @@ func home(ctx *context.Context, viewRepositories bool) {
|
||||
ctx.ServerError("FindOrgMembers", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Members"] = members
|
||||
ctx.Data["Teams"] = ctx.Org.Teams
|
||||
|
||||
const orgOverviewTeamsLimit = 5
|
||||
ctx.Data["OrgOverviewMembers"] = members
|
||||
// The overview widget shows only teams the viewer belongs to. ctx.Org.Teams
|
||||
// may include visible-but-not-joined teams (via IncludeVisibilities for
|
||||
// signed-in non-members), so re-query the viewer's own membership; owners
|
||||
// keep the full list they are entitled to manage.
|
||||
overviewTeams := ctx.Org.Teams
|
||||
if !ctx.Org.IsOwner {
|
||||
overviewTeams = nil
|
||||
if ctx.Org.IsMember {
|
||||
overviewTeams, _, err = organization.SearchTeam(ctx, &organization.SearchTeamOptions{
|
||||
OrgID: org.ID,
|
||||
UserID: ctx.Doer.ID,
|
||||
ListOptions: db.ListOptions{Page: 1, PageSize: orgOverviewTeamsLimit},
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("SearchTeam", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.Data["OrgOverviewTeams"] = overviewTeams[:min(len(overviewTeams), orgOverviewTeamsLimit)]
|
||||
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
|
||||
ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0
|
||||
|
||||
@@ -180,7 +200,7 @@ func prepareOrgProfileReadme(ctx *context.Context, prepareResult *shared_user.Pr
|
||||
}
|
||||
|
||||
rctx := renderhelper.NewRenderContextRepoFile(ctx, profileRepo, renderhelper.RepoFileOptions{
|
||||
CurrentRefPath: path.Join("branch", util.PathEscapeSegments(profileRepo.DefaultBranch)),
|
||||
CurrentRefSubURL: git.RefNameFromBranch(profileRepo.DefaultBranch).RefWebLinkPath(),
|
||||
})
|
||||
ctx.Data["ProfileReadmeContent"], err = markdown.RenderString(rctx, readmeBytes)
|
||||
if err != nil {
|
||||
|
||||
@@ -6,7 +6,7 @@ package org_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"gitea.dev/models/unittest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
||||
@@ -8,15 +8,15 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
org_service "code.gitea.io/gitea/services/org"
|
||||
"gitea.dev/models/organization"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
"gitea.dev/services/context"
|
||||
org_service "gitea.dev/services/org"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -31,16 +31,19 @@ func Members(ctx *context.Context) {
|
||||
ctx.Data["PageIsOrgMembers"] = true
|
||||
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
keyword := ctx.FormTrim("q")
|
||||
ctx.Data["Keyword"] = keyword
|
||||
|
||||
opts := &organization.FindOrgMembersOpts{
|
||||
Doer: ctx.Doer,
|
||||
OrgID: org.ID,
|
||||
Doer: ctx.Doer,
|
||||
OrgID: org.ID,
|
||||
Keyword: keyword,
|
||||
}
|
||||
|
||||
if ctx.Doer != nil {
|
||||
isMember, err := ctx.Org.Organization.IsOrgMember(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "IsOrgMember")
|
||||
ctx.ServerError("IsOrgMember", err)
|
||||
return
|
||||
}
|
||||
opts.IsDoerMember = isMember
|
||||
@@ -49,7 +52,7 @@ func Members(ctx *context.Context) {
|
||||
|
||||
total, err := organization.CountOrgMembers(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.HTTPError(http.StatusInternalServerError, "CountOrgMembers")
|
||||
ctx.ServerError("CountOrgMembers", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -58,9 +61,11 @@ func Members(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
pager := context.NewPagination(total, setting.UI.MembersPagingNum, page, 5)
|
||||
opts.ListOptions.Page = page
|
||||
opts.ListOptions.PageSize = setting.UI.MembersPagingNum
|
||||
pageSize := setting.UI.MembersPagingNum
|
||||
pager := context.NewPagination(total, pageSize, page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
opts.ListOptions.Page = pager.Paginater.Current()
|
||||
opts.ListOptions.PageSize = pageSize
|
||||
members, membersIsPublic, err := organization.FindOrgMembers(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetMembers", err)
|
||||
|
||||
@@ -6,10 +6,10 @@ package org
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
shared_mention "code.gitea.io/gitea/routers/web/shared/mention"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/modules/util"
|
||||
shared_mention "gitea.dev/routers/web/shared/mention"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
// GetMentionsInOwner returns JSON data for mention autocomplete on owner-level pages.
|
||||
|
||||
@@ -8,15 +8,15 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/models/organization"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -6,15 +6,15 @@ package org
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/label"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
shared_label "code.gitea.io/gitea/routers/web/shared/label"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"gitea.dev/models/db"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/modules/label"
|
||||
repo_module "gitea.dev/modules/repository"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
shared_label "gitea.dev/routers/web/shared/label"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
)
|
||||
|
||||
// RetrieveLabels find all the labels of an organization
|
||||
|
||||
@@ -9,21 +9,21 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/web/shared/issue"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
project_service "code.gitea.io/gitea/services/projects"
|
||||
"gitea.dev/models/db"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
project_model "gitea.dev/models/project"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/modules/json"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/web/shared/issue"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
project_service "gitea.dev/services/projects"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
@@ -34,14 +34,6 @@ const (
|
||||
tplProjectsView templates.TplName = "org/projects/view"
|
||||
)
|
||||
|
||||
// MustEnableProjects check if projects are enabled in settings
|
||||
func MustEnableProjects(ctx *context.Context) {
|
||||
if unit.TypeProjects.UnitGlobalDisabled() {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Projects renders the home page of projects
|
||||
func Projects(ctx *context.Context) {
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
@@ -458,7 +450,7 @@ func ViewProject(ctx *context.Context) {
|
||||
ctx.Data["MilestoneID"] = milestoneID
|
||||
|
||||
// Get assignees.
|
||||
assigneeUsers, err := project_service.LoadIssuesAssigneesForProject(ctx, issuesMap)
|
||||
assigneeUsers, err := project_service.LoadIssuesAssigneesForProject(ctx, project.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadIssuesAssigneesForProject", err)
|
||||
return
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/web/org"
|
||||
"code.gitea.io/gitea/services/contexttest"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/web/org"
|
||||
"gitea.dev/services/contexttest"
|
||||
"gitea.dev/services/forms"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -9,25 +9,25 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
user_setting "code.gitea.io/gitea/routers/web/user/setting"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
org_service "code.gitea.io/gitea/services/org"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
"gitea.dev/models/db"
|
||||
packages_model "gitea.dev/models/packages"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/models/webhook"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
repo_module "gitea.dev/modules/repository"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
user_setting "gitea.dev/routers/web/user/setting"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
org_service "gitea.dev/services/org"
|
||||
user_service "gitea.dev/services/user"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -7,13 +7,13 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
user_setting "code.gitea.io/gitea/routers/web/user/setting"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/models/auth"
|
||||
"gitea.dev/models/db"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
user_setting "gitea.dev/routers/web/user/setting"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
shared "code.gitea.io/gitea/routers/web/shared/packages"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
shared "gitea.dev/routers/web/shared/packages"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -13,22 +13,24 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
org_model "code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
unit_model "code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
org_service "code.gitea.io/gitea/services/org"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
"gitea.dev/models/db"
|
||||
org_model "gitea.dev/models/organization"
|
||||
"gitea.dev/models/perm"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
unit_model "gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
"gitea.dev/services/forms"
|
||||
org_service "gitea.dev/services/org"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -54,13 +56,56 @@ func Teams(ctx *context.Context) {
|
||||
ctx.Data["Title"] = org.FullName
|
||||
ctx.Data["PageIsOrgTeams"] = true
|
||||
|
||||
for _, t := range ctx.Org.Teams {
|
||||
keyword := ctx.FormTrim("q")
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
pagingNum := setting.UI.MembersPagingNum
|
||||
|
||||
searchTeams := func() (teams []*org_model.Team, count int64, err error) {
|
||||
if keyword == "" {
|
||||
// fast path, use existing teams in context if no need to filter from database
|
||||
count = int64(len(ctx.Org.Teams))
|
||||
start := (page - 1) * pagingNum
|
||||
if start > len(ctx.Org.Teams) {
|
||||
return nil, count, nil
|
||||
}
|
||||
end := min(start+pagingNum, len(ctx.Org.Teams))
|
||||
return ctx.Org.Teams[start:end], count, nil
|
||||
}
|
||||
|
||||
shouldSeeAllOrgTeams, err := context.UserShouldSeeAllOrgTeams(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
opts := &org_model.SearchTeamOptions{
|
||||
OrgID: org.ID,
|
||||
UserID: util.Iif(shouldSeeAllOrgTeams, 0, ctx.Doer.ID),
|
||||
Keyword: keyword,
|
||||
IncludeDesc: true,
|
||||
IncludeVisibilities: util.Iif(shouldSeeAllOrgTeams, nil,
|
||||
org_model.VisibleTeamVisibilitiesFor(ctx.Org.IsMember, ctx.IsSigned)),
|
||||
ListOptions: db.ListOptions{Page: page, PageSize: pagingNum},
|
||||
}
|
||||
return org_model.SearchTeam(ctx, opts)
|
||||
}
|
||||
|
||||
teams, count, err := searchTeams()
|
||||
if err != nil {
|
||||
ctx.ServerError("SearchTeam", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, t := range teams {
|
||||
if err := t.LoadMembers(ctx); err != nil {
|
||||
ctx.ServerError("GetMembers", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.Data["Teams"] = ctx.Org.Teams
|
||||
|
||||
ctx.Data["OrgListTeams"] = teams
|
||||
ctx.Data["Keyword"] = keyword
|
||||
pager := context.NewPagination(count, setting.UI.MembersPagingNum, page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.HTML(http.StatusOK, tplTeams)
|
||||
}
|
||||
@@ -213,7 +258,7 @@ func checkIsOrgMemberAndRedirect(ctx *context.Context, defaultRedirect string) {
|
||||
if isOrgMember, err := org_model.IsOrganizationMember(ctx, ctx.Org.Organization.ID, ctx.Doer.ID); err != nil {
|
||||
ctx.ServerError("IsOrganizationMember", err)
|
||||
return
|
||||
} else if !isOrgMember {
|
||||
} else if !isOrgMember && !ctx.Doer.IsAdmin {
|
||||
if ctx.Org.Organization.Visibility.IsPrivate() {
|
||||
defaultRedirect = setting.AppSubURL + "/"
|
||||
} else {
|
||||
@@ -335,6 +380,7 @@ func NewTeamPost(ctx *context.Context) {
|
||||
AccessMode: teamPermission,
|
||||
IncludesAllRepositories: includesAllRepositories,
|
||||
CanCreateOrgRepo: form.CanCreateOrgRepo,
|
||||
Visibility: org_model.NormalizeTeamVisibility(form.Visibility),
|
||||
}
|
||||
|
||||
units := make([]*org_model.TeamUnit, 0, len(unitPerms))
|
||||
@@ -435,13 +481,22 @@ func SearchTeam(ctx *context.Context) {
|
||||
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
|
||||
}
|
||||
|
||||
shouldSeeAll, err := context.UserShouldSeeAllOrgTeams(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("UserShouldSeeAllOrgTeams", err)
|
||||
return
|
||||
}
|
||||
|
||||
opts := &org_model.SearchTeamOptions{
|
||||
// UserID is not set because the router already requires the doer to be an org admin. Thus, we don't need to restrict to teams that the user belongs in
|
||||
Keyword: ctx.FormTrim("q"),
|
||||
OrgID: ctx.Org.Organization.ID,
|
||||
IncludeDesc: ctx.FormString("include_desc") == "" || ctx.FormBool("include_desc"),
|
||||
ListOptions: listOptions,
|
||||
}
|
||||
if !shouldSeeAll {
|
||||
opts.UserID = ctx.Doer.ID
|
||||
opts.IncludeVisibilities = org_model.VisibleTeamVisibilitiesFor(ctx.Org.IsMember, ctx.IsSigned)
|
||||
}
|
||||
|
||||
teams, maxResults, err := org_model.SearchTeam(ctx, opts)
|
||||
if err != nil {
|
||||
@@ -514,8 +569,11 @@ func EditTeamPost(ctx *context.Context) {
|
||||
t.IncludesAllRepositories = includesAllRepositories
|
||||
}
|
||||
t.CanCreateOrgRepo = form.CanCreateOrgRepo
|
||||
t.Visibility = org_model.NormalizeTeamVisibility(form.Visibility)
|
||||
} else {
|
||||
t.CanCreateOrgRepo = true
|
||||
// The owner team must remain listable to all org members.
|
||||
t.Visibility = structs.VisibleTypeLimited
|
||||
}
|
||||
|
||||
t.Description = form.Description
|
||||
@@ -567,6 +625,8 @@ func DeleteTeam(ctx *context.Context) {
|
||||
// TeamInvite renders the team invite page
|
||||
func TeamInvite(ctx *context.Context) {
|
||||
invite, org, team, inviter, err := getTeamInviteFromContext(ctx)
|
||||
// TODO: to quickly debug the UI, can uncomment this (don't worry, it won't pass CI lint)
|
||||
// invite, org, team, inviter, err = &org_model.TeamInvite{}, &org_model.Organization{}, &org_model.Team{}, ctx.Doer, nil
|
||||
if err != nil {
|
||||
if org_model.IsErrTeamInviteNotFound(err) {
|
||||
ctx.NotFound(err)
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/models/organization"
|
||||
"gitea.dev/modules/templates"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const tplByRepos templates.TplName = "org/worktime"
|
||||
|
||||
@@ -6,8 +6,8 @@ package web
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
type passkeyEndpointsType struct {
|
||||
|
||||
@@ -4,31 +4,34 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
stdCtx "context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/actions"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
actions_model "gitea.dev/models/actions"
|
||||
"gitea.dev/models/db"
|
||||
git_model "gitea.dev/models/git"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/modules/actions"
|
||||
"gitea.dev/modules/actions/jobparser"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
shared_user "gitea.dev/routers/web/shared/user"
|
||||
actions_service "gitea.dev/services/actions"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/convert"
|
||||
|
||||
act_model "github.com/nektos/act/pkg/model"
|
||||
act_model "gitea.com/gitea/runner/act/model"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
@@ -44,6 +47,20 @@ type WorkflowInfo struct {
|
||||
Workflow *act_model.Workflow
|
||||
}
|
||||
|
||||
type workflowBadge struct {
|
||||
DisplayName string
|
||||
BadgeURL string
|
||||
WorkflowURL string
|
||||
}
|
||||
|
||||
// DisplayName returns the workflow name from the YAML file if present, otherwise the filename.
|
||||
func (w WorkflowInfo) DisplayName() string {
|
||||
if w.Workflow != nil && w.Workflow.Name != "" {
|
||||
return w.Workflow.Name
|
||||
}
|
||||
return w.Entry.Name()
|
||||
}
|
||||
|
||||
// MustEnableActions check if actions are enabled in settings
|
||||
func MustEnableActions(ctx *context.Context) {
|
||||
if !setting.Actions.Enabled {
|
||||
@@ -57,7 +74,7 @@ func MustEnableActions(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if ctx.Repo.Repository != nil {
|
||||
if !ctx.Repo.CanRead(unit.TypeActions) {
|
||||
if !ctx.Repo.Permission.CanRead(unit.TypeActions) {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
@@ -82,12 +99,22 @@ func List(ctx *context.Context) {
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID)
|
||||
curWorkflowRepoID := ctx.FormInt64("scoped_workflow_source_repo_id")
|
||||
ctx.Data["CurWorkflowRepoID"] = curWorkflowRepoID
|
||||
scopedNames := prepareScopedWorkflows(ctx, curWorkflowID, curWorkflowRepoID)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
otherWorkflows := prepareOtherWorkflows(ctx, workflows, scopedNames, curWorkflowID)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID, curWorkflowRepoID)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
prepareWorkflowList(ctx, workflows)
|
||||
prepareWorkflowList(ctx, workflows, otherWorkflows, len(scopedNames) > 0)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@@ -95,6 +122,33 @@ func List(ctx *context.Context) {
|
||||
ctx.HTML(http.StatusOK, tplListActions)
|
||||
}
|
||||
|
||||
// prepareOtherWorkflows surfaces historical runs whose workflow file no longer
|
||||
// exists on the default branch (renamed, removed, or only on other branches).
|
||||
func prepareOtherWorkflows(ctx *context.Context, workflows []WorkflowInfo, scopedNames container.Set[string], curWorkflowID string) []string {
|
||||
listed := make(container.Set[string], len(workflows))
|
||||
for _, w := range workflows {
|
||||
listed.Add(w.Entry.Name())
|
||||
}
|
||||
|
||||
var other []string
|
||||
if ctx.Repo.Repository.NumActionRuns > 0 {
|
||||
// "Other workflows" lists repo-level orphans only: GetRepoRunWorkflowIDs excludes scoped runs.
|
||||
ids, err := actions_model.GetRepoRunWorkflowIDs(ctx, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRepoRunWorkflowIDs", err)
|
||||
return nil
|
||||
}
|
||||
other = container.FilterSlice(ids, func(id string) (string, bool) {
|
||||
return id, id != "" && !listed.Contains(id)
|
||||
})
|
||||
}
|
||||
|
||||
ctx.Data["OtherWorkflows"] = other
|
||||
// A selected workflow counts as "listed" if it is a repo-level file or an active scoped workflow.
|
||||
ctx.Data["CurWorkflowIsListed"] = curWorkflowID == "" || listed.Contains(curWorkflowID) || scopedNames.Contains(curWorkflowID)
|
||||
return other
|
||||
}
|
||||
|
||||
func WorkflowDispatchInputs(ctx *context.Context) {
|
||||
ref := ctx.FormString("ref")
|
||||
if ref == "" {
|
||||
@@ -121,7 +175,7 @@ func WorkflowDispatchInputs(ctx *context.Context) {
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID)
|
||||
prepareWorkflowDispatchTemplate(ctx, workflows, curWorkflowID, ctx.FormInt64("scoped_workflow_source_repo_id"))
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@@ -145,7 +199,7 @@ func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflow
|
||||
ctx.ServerError("GetContentFromEntry", err)
|
||||
return nil, ""
|
||||
}
|
||||
wf, err := act_model.ReadWorkflow(bytes.NewReader(content))
|
||||
wf, err := jobparser.ReadWorkflow(content)
|
||||
if err != nil {
|
||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error())
|
||||
workflows = append(workflows, workflow)
|
||||
@@ -169,19 +223,28 @@ func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflow
|
||||
if !hasJobWithoutNeeds && len(j.Needs()) == 0 {
|
||||
hasJobWithoutNeeds = true
|
||||
}
|
||||
if j.Uses != "" {
|
||||
if _, err := actions_service.ResolveUses(ctx, j.Uses); err != nil {
|
||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.invalid_reusable_workflow_uses", err.Error())
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasJobWithoutNeeds {
|
||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs")
|
||||
}
|
||||
if emptyJobsNumber == len(wf.Jobs) {
|
||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job")
|
||||
if workflow.ErrMsg == "" {
|
||||
if !hasJobWithoutNeeds {
|
||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job_without_needs")
|
||||
}
|
||||
if emptyJobsNumber == len(wf.Jobs) {
|
||||
workflow.ErrMsg = ctx.Locale.TrString("actions.runs.no_job")
|
||||
}
|
||||
}
|
||||
workflows = append(workflows, workflow)
|
||||
}
|
||||
|
||||
ctx.Data["workflows"] = workflows
|
||||
ctx.Data["RepoLink"] = ctx.Repo.Repository.Link()
|
||||
ctx.Data["AllowDisableOrEnableWorkflow"] = ctx.Repo.IsAdmin()
|
||||
ctx.Data["RepoID"] = ctx.Repo.Repository.ID
|
||||
ctx.Data["AllowDisableOrEnableWorkflow"] = ctx.Repo.Permission.IsAdmin()
|
||||
actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
|
||||
ctx.Data["ActionsConfig"] = actionsConfig
|
||||
ctx.Data["CurWorkflow"] = curWorkflowID
|
||||
@@ -190,21 +253,165 @@ func prepareWorkflowTemplate(ctx *context.Context, commit *git.Commit) (workflow
|
||||
return workflows, curWorkflowID
|
||||
}
|
||||
|
||||
func prepareWorkflowDispatchTemplate(ctx *context.Context, workflowInfos []WorkflowInfo, curWorkflowID string) {
|
||||
actionsConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
|
||||
if curWorkflowID == "" || !ctx.Repo.CanWrite(unit.TypeActions) || actionsConfig.IsWorkflowDisabled(curWorkflowID) {
|
||||
// ScopedWorkflowInfo describes a scoped workflow effective for the current repo, listed under its source group.
|
||||
type ScopedWorkflowInfo struct {
|
||||
SourceRepoID int64
|
||||
EntryName string
|
||||
DisplayName string
|
||||
Required bool
|
||||
Disabled bool
|
||||
}
|
||||
|
||||
// ScopedWorkflowSourceGroup groups the scoped workflows contributed by one source repo for the All-Workflows sidebar.
|
||||
type ScopedWorkflowSourceGroup struct {
|
||||
SourceRepoID int64
|
||||
SourceRepoName string // owner/name of the source repo; shown for instance-level sources and used as the tooltip
|
||||
SourceRepoShortName string // name only; shown for owner-level sources, where the owner is always the current owner
|
||||
FromInstance bool // registered at instance level (owner_id == 0) rather than by the owner
|
||||
IsActive bool // the currently-selected workflow belongs to this source; render the group expanded
|
||||
Workflows []ScopedWorkflowInfo
|
||||
}
|
||||
|
||||
// prepareScopedWorkflows lists the scoped workflows effective for the repo's owner (and instance) for the All-Workflows sidebar.
|
||||
func prepareScopedWorkflows(ctx *context.Context, curWorkflowID string, curWorkflowRepoID int64) container.Set[string] {
|
||||
scopedNames := make(container.Set[string])
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
sources, err := actions_model.GetEffectiveScopedWorkflowSources(ctx, repo.OwnerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetEffectiveScopedWorkflowSources", err)
|
||||
return scopedNames
|
||||
}
|
||||
if len(sources) == 0 {
|
||||
return scopedNames
|
||||
}
|
||||
|
||||
actionsConfig := repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
|
||||
|
||||
groups := make([]ScopedWorkflowSourceGroup, 0, len(sources))
|
||||
seen := make(map[int64]bool, len(sources))
|
||||
for _, source := range sources {
|
||||
if seen[source.SourceRepoID] {
|
||||
continue
|
||||
}
|
||||
seen[source.SourceRepoID] = true
|
||||
|
||||
sourceRepo, err := repo_model.GetRepositoryByID(ctx, source.SourceRepoID)
|
||||
if err != nil {
|
||||
log.Error("scoped workflows list: load source repo %d: %v", source.SourceRepoID, err)
|
||||
continue
|
||||
}
|
||||
if sourceRepo.IsEmpty {
|
||||
continue
|
||||
}
|
||||
|
||||
_, entries, err := actions_service.LoadParsedScopedWorkflows(ctx, sourceRepo)
|
||||
if err != nil {
|
||||
log.Error("scoped workflows list: parse %s: %v", sourceRepo.FullName(), err)
|
||||
continue
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
group := ScopedWorkflowSourceGroup{
|
||||
SourceRepoID: sourceRepo.ID,
|
||||
SourceRepoName: sourceRepo.FullName(),
|
||||
SourceRepoShortName: sourceRepo.Name,
|
||||
FromInstance: source.OwnerID == 0,
|
||||
}
|
||||
for _, e := range entries {
|
||||
scopedNames.Add(e.EntryName)
|
||||
required := actions_model.IsWorkflowRequiredInSources(sources, sourceRepo.ID, e.EntryName)
|
||||
disabled := actionsConfig.IsScopedWorkflowDisabled(sourceRepo.ID, e.EntryName)
|
||||
group.Workflows = append(group.Workflows, ScopedWorkflowInfo{
|
||||
SourceRepoID: sourceRepo.ID,
|
||||
EntryName: e.EntryName,
|
||||
DisplayName: e.DisplayName,
|
||||
Required: required,
|
||||
Disabled: disabled,
|
||||
})
|
||||
|
||||
if curWorkflowID == e.EntryName && curWorkflowRepoID == sourceRepo.ID {
|
||||
ctx.Data["CurWorkflowDisabled"] = disabled
|
||||
ctx.Data["CurWorkflowScopedRepoID"] = sourceRepo.ID
|
||||
ctx.Data["CurWorkflowRequired"] = required
|
||||
group.IsActive = true // keep this group expanded so the selected workflow stays visible
|
||||
}
|
||||
}
|
||||
groups = append(groups, group)
|
||||
}
|
||||
|
||||
ctx.Data["ScopedWorkflowGroups"] = groups
|
||||
return scopedNames
|
||||
}
|
||||
|
||||
// loadScopedWorkflowModel reads and parses a scoped workflow's content from its source repo's default branch.
|
||||
func loadScopedWorkflowModel(ctx *context.Context, repo *repo_model.Repository, sourceRepoID int64, workflowID string) *act_model.Workflow {
|
||||
effective, err := actions_model.IsScopedWorkflowSourceEffective(ctx, repo.OwnerID, sourceRepoID)
|
||||
if err != nil {
|
||||
log.Error("scoped dispatch: IsScopedWorkflowSourceEffective: %v", err)
|
||||
return nil
|
||||
}
|
||||
if !effective {
|
||||
return nil
|
||||
}
|
||||
|
||||
sourceRepo, err := repo_model.GetRepositoryByID(ctx, sourceRepoID)
|
||||
if err != nil || sourceRepo.IsEmpty {
|
||||
return nil
|
||||
}
|
||||
content, err := actions_service.ScopedWorkflowContent(ctx, sourceRepo, workflowID)
|
||||
if err != nil {
|
||||
log.Error("scoped dispatch: content of %s in %s: %v", workflowID, sourceRepo.RelativePath(), err)
|
||||
return nil
|
||||
}
|
||||
if content == nil {
|
||||
return nil // the workflow does not exist on the source's default branch
|
||||
}
|
||||
wf, err := jobparser.ReadWorkflow(content)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return wf
|
||||
}
|
||||
|
||||
func prepareWorkflowDispatchTemplate(ctx *context.Context, workflowInfos []WorkflowInfo, curWorkflowID string, curWorkflowRepoID int64) {
|
||||
repo := ctx.Repo.Repository
|
||||
if curWorkflowID == "" || !ctx.Repo.Permission.CanWrite(unit.TypeActions) {
|
||||
return
|
||||
}
|
||||
actionsConfig := repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
|
||||
|
||||
isScoped := curWorkflowRepoID > 0
|
||||
if isScoped {
|
||||
// a required scoped workflow can never be opted out, so a stale disabled flag must not hide its dispatch form
|
||||
optedOut, err := actions_model.IsScopedWorkflowOptedOut(ctx, actionsConfig, repo.OwnerID, curWorkflowRepoID, curWorkflowID)
|
||||
if err != nil {
|
||||
log.Error("IsScopedWorkflowOptedOut: %v", err)
|
||||
return
|
||||
}
|
||||
if optedOut {
|
||||
return
|
||||
}
|
||||
} else if actionsConfig.IsWorkflowDisabled(curWorkflowID) {
|
||||
return
|
||||
}
|
||||
|
||||
var curWorkflow *act_model.Workflow
|
||||
for _, workflowInfo := range workflowInfos {
|
||||
if workflowInfo.Entry.Name() == curWorkflowID {
|
||||
if workflowInfo.Workflow == nil {
|
||||
log.Debug("CurWorkflowID %s is found but its workflowInfo.Workflow is nil", curWorkflowID)
|
||||
return
|
||||
if isScoped {
|
||||
// a scoped workflow's content lives in its source repo, not in workflowInfos (the consumer's own files)
|
||||
curWorkflow = loadScopedWorkflowModel(ctx, repo, curWorkflowRepoID, curWorkflowID)
|
||||
} else {
|
||||
for _, workflowInfo := range workflowInfos {
|
||||
if workflowInfo.Entry.Name() == curWorkflowID {
|
||||
if workflowInfo.Workflow == nil {
|
||||
log.Debug("CurWorkflowID %s is found but its workflowInfo.Workflow is nil", curWorkflowID)
|
||||
return
|
||||
}
|
||||
curWorkflow = workflowInfo.Workflow
|
||||
break
|
||||
}
|
||||
curWorkflow = workflowInfo.Workflow
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,10 +452,12 @@ func prepareWorkflowDispatchTemplate(ctx *context.Context, workflowInfos []Workf
|
||||
ctx.Data["Tags"] = tags
|
||||
}
|
||||
|
||||
func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) {
|
||||
func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo, otherWorkflows []string, hasScopedWorkflows bool) {
|
||||
actorID := ctx.FormInt64("actor")
|
||||
status := ctx.FormInt("status")
|
||||
workflowID := ctx.FormString("workflow")
|
||||
scopedWorkflowSourceRepoID := ctx.FormInt64("scoped_workflow_source_repo_id")
|
||||
branch := ctx.FormString("branch")
|
||||
page := ctx.FormInt("page")
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
@@ -258,7 +467,8 @@ func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) {
|
||||
// they will be 0 by default, which indicates get all status or actors
|
||||
ctx.Data["CurActor"] = actorID
|
||||
ctx.Data["CurStatus"] = status
|
||||
if actorID > 0 || status > int(actions_model.StatusUnknown) {
|
||||
ctx.Data["CurBranch"] = branch
|
||||
if actorID > 0 || status > int(actions_model.StatusUnknown) || branch != "" {
|
||||
ctx.Data["IsFiltered"] = true
|
||||
}
|
||||
|
||||
@@ -267,15 +477,24 @@ func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) {
|
||||
Page: page,
|
||||
PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
|
||||
},
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
WorkflowID: workflowID,
|
||||
TriggerUserID: actorID,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
WorkflowID: workflowID,
|
||||
WorkflowRepoID: scopedWorkflowSourceRepoID,
|
||||
TriggerUserID: actorID,
|
||||
}
|
||||
|
||||
// Constrain scoped vs repo-level only for a listed workflow, whose link carries scoped_workflow_source_repo_id.
|
||||
if workflowID != "" && !slices.Contains(otherWorkflows, workflowID) {
|
||||
opts.IsScopedRun = optional.Some(scopedWorkflowSourceRepoID > 0)
|
||||
}
|
||||
|
||||
// if status is not StatusUnknown, it means user has selected a status filter
|
||||
if actions_model.Status(status) != actions_model.StatusUnknown {
|
||||
opts.Status = []actions_model.Status{actions_model.Status(status)}
|
||||
}
|
||||
if branch != "" {
|
||||
opts.Ref = string(git.RefNameFromBranch(branch))
|
||||
}
|
||||
|
||||
runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, opts)
|
||||
if err != nil {
|
||||
@@ -308,32 +527,40 @@ func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) {
|
||||
return
|
||||
}
|
||||
for _, run := range runs {
|
||||
if !run.Status.In(actions_model.StatusWaiting, actions_model.StatusRunning) {
|
||||
if !run.Status.In(actions_model.StatusWaiting, actions_model.StatusRunning, actions_model.StatusBlocked) {
|
||||
continue
|
||||
}
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
||||
jobs, err := actions_model.GetLatestAttemptJobsByRepoAndRunID(ctx, run.RepoID, run.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRunJobsByRunID", err)
|
||||
return
|
||||
}
|
||||
for _, job := range jobs {
|
||||
if !job.Status.IsWaiting() {
|
||||
if !job.Status.In(actions_model.StatusWaiting, actions_model.StatusBlocked) {
|
||||
continue
|
||||
}
|
||||
if err := actions.ValidateWorkflowContent(job.WorkflowPayload); err != nil {
|
||||
runErrors[run.ID] = ctx.Locale.TrString("actions.runs.invalid_workflow_helper", err.Error())
|
||||
break
|
||||
}
|
||||
hasOnlineRunner := false
|
||||
for _, runner := range runners {
|
||||
if !runner.IsDisabled && runner.CanMatchLabels(job.RunsOn) {
|
||||
hasOnlineRunner = true
|
||||
if job.CallUses != "" {
|
||||
if _, err := actions_service.ResolveUses(ctx, job.CallUses); err != nil {
|
||||
runErrors[run.ID] = ctx.Locale.TrString("actions.runs.invalid_reusable_workflow_uses", err.Error())
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasOnlineRunner {
|
||||
runErrors[run.ID] = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", strings.Join(job.RunsOn, ","))
|
||||
break
|
||||
if job.Status.IsWaiting() {
|
||||
hasOnlineRunner := false
|
||||
for _, runner := range runners {
|
||||
if !runner.IsDisabled && runner.CanMatchLabels(job.RunsOn) {
|
||||
hasOnlineRunner = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasOnlineRunner {
|
||||
runErrors[run.ID] = ctx.Locale.TrString("actions.runs.no_matching_online_runner_helper", strings.Join(job.RunsOn, ","))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -341,6 +568,22 @@ func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) {
|
||||
|
||||
ctx.Data["Runs"] = runs
|
||||
|
||||
workflowNames := make(map[string]string, len(workflows))
|
||||
workflowDisplayName := workflowID
|
||||
for _, wf := range workflows {
|
||||
displayName := wf.DisplayName()
|
||||
workflowNames[wf.Entry.Name()] = displayName
|
||||
if wf.Entry.Name() == workflowID {
|
||||
workflowDisplayName = displayName
|
||||
}
|
||||
}
|
||||
ctx.Data["WorkflowNames"] = workflowNames
|
||||
// A scoped workflow has no repo-level badge on this repo (the badge endpoint reads is_scoped_run=false runs),
|
||||
// so don't offer the "create status badge" entry for it.
|
||||
if scopedWorkflowSourceRepoID == 0 {
|
||||
prepareWorkflowBadgeTemplate(ctx, workflowID, workflowDisplayName)
|
||||
}
|
||||
|
||||
actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetActors", err)
|
||||
@@ -350,12 +593,36 @@ func prepareWorkflowList(ctx *context.Context, workflows []WorkflowInfo) {
|
||||
|
||||
ctx.Data["StatusInfoList"] = actions_model.GetStatusInfoList(ctx, ctx.Locale)
|
||||
|
||||
runBranches, err := actions_model.GetRunBranches(ctx, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetRunBranches", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["RunBranches"] = runBranches
|
||||
|
||||
pager := context.NewPagination(total, opts.PageSize, opts.Page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0
|
||||
ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(otherWorkflows) > 0 || len(runs) > 0 || hasScopedWorkflows
|
||||
|
||||
ctx.Data["CanWriteRepoUnitActions"] = ctx.Repo.CanWrite(unit.TypeActions)
|
||||
ctx.Data["CanWriteRepoUnitActions"] = ctx.Repo.Permission.CanWrite(unit.TypeActions)
|
||||
}
|
||||
|
||||
func prepareWorkflowBadgeTemplate(ctx *context.Context, workflowID, displayName string) {
|
||||
if workflowID == "" {
|
||||
return
|
||||
}
|
||||
displayName = util.IfZero(displayName, workflowID)
|
||||
|
||||
repoURL := ctx.Repo.Repository.HTMLURL(ctx)
|
||||
badgeURL := fmt.Sprintf("%s/actions/workflows/%s/badge.svg?branch=%s", repoURL, util.PathEscapeSegments(workflowID), url.QueryEscape(ctx.Repo.Repository.DefaultBranch))
|
||||
workflowURL := fmt.Sprintf("%s/actions?workflow=%s", repoURL, url.QueryEscape(workflowID))
|
||||
|
||||
ctx.Data["WorkflowBadge"] = workflowBadge{
|
||||
BadgeURL: badgeURL,
|
||||
WorkflowURL: workflowURL,
|
||||
DisplayName: displayName,
|
||||
}
|
||||
}
|
||||
|
||||
// loadIsRefDeleted loads the IsRefDeleted field for each run in the list.
|
||||
@@ -395,6 +662,10 @@ type WorkflowDispatchInput struct {
|
||||
Options []string `yaml:"options"`
|
||||
}
|
||||
|
||||
func (i WorkflowDispatchInput) IsDefaultTrue() bool {
|
||||
return util.ParseYamlBool(i.Default)
|
||||
}
|
||||
|
||||
type WorkflowDispatch struct {
|
||||
Inputs []WorkflowDispatchInput
|
||||
}
|
||||
@@ -466,3 +737,14 @@ func decodeNode(node yaml.Node, out any) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func actionsListRedirectURL(repoLink, workflow, scopedWorkflowSourceRepoID, actor, status, branch string) string {
|
||||
return fmt.Sprintf("%s/actions?workflow=%s&scoped_workflow_source_repo_id=%s&actor=%s&status=%s&branch=%s",
|
||||
repoLink,
|
||||
url.QueryEscape(workflow),
|
||||
url.QueryEscape(scopedWorkflowSourceRepoID),
|
||||
url.QueryEscape(actor),
|
||||
url.QueryEscape(status),
|
||||
url.QueryEscape(branch),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,14 +4,20 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
unittest "code.gitea.io/gitea/models/unittest"
|
||||
actions_model "gitea.dev/models/actions"
|
||||
"gitea.dev/models/db"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
unittest "gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
web_context "gitea.dev/services/context"
|
||||
|
||||
act_model "github.com/nektos/act/pkg/model"
|
||||
act_model "gitea.com/gitea/runner/act/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -176,3 +182,41 @@ func Test_loadIsRefDeleted(t *testing.T) {
|
||||
assert.True(t, run.IsRefDeleted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareWorkflowBadgeTemplate(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.AppURL, "https://gitea.example.com/")()
|
||||
|
||||
t.Run("no workflow selected", func(t *testing.T) {
|
||||
ctx := newWorkflowBadgeTestContext(t)
|
||||
|
||||
prepareWorkflowBadgeTemplate(ctx, "", "ignored")
|
||||
|
||||
assert.NotContains(t, ctx.Data, "WorkflowBadge")
|
||||
})
|
||||
|
||||
t.Run("selected workflow", func(t *testing.T) {
|
||||
ctx := newWorkflowBadgeTestContext(t)
|
||||
|
||||
prepareWorkflowBadgeTemplate(ctx, "build/test workflow.yml", `CI [prod]\build "fast" <ok>`)
|
||||
|
||||
assert.Equal(t, workflowBadge{
|
||||
BadgeURL: "https://gitea.example.com/user1/repo1/actions/workflows/build/test%20workflow.yml/badge.svg?branch=release%2F1.0+%26+hotfix",
|
||||
WorkflowURL: "https://gitea.example.com/user1/repo1/actions?workflow=build%2Ftest+workflow.yml",
|
||||
DisplayName: `CI [prod]\build "fast" <ok>`,
|
||||
}, ctx.Data["WorkflowBadge"])
|
||||
})
|
||||
}
|
||||
|
||||
func newWorkflowBadgeTestContext(t *testing.T) *web_context.Context {
|
||||
t.Helper()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "https://gitea.example.com/user1/repo1/actions", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
ctx := web_context.NewWebContext(web_context.NewBaseContextForTest(resp, req), nil, nil)
|
||||
ctx.Repo.Repository = &repo_model.Repository{
|
||||
OwnerName: "user1",
|
||||
Name: "repo1",
|
||||
DefaultBranch: "release/1.0 & hotfix",
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -9,14 +9,20 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/modules/badge"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
actions_model "gitea.dev/models/actions"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/modules/badge"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
func GetWorkflowBadge(ctx *context.Context) {
|
||||
context.CheckRepoScopedToken(ctx, ctx.Repo.Repository, auth_model.Read)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
workflowFile := ctx.PathParam("workflow_name")
|
||||
branch := ctx.FormString("branch", ctx.Repo.Repository.DefaultBranch)
|
||||
event := ctx.FormString("event")
|
||||
|
||||
@@ -6,7 +6,7 @@ package actions
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"gitea.dev/models/unittest"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,14 +6,76 @@ package actions
|
||||
import (
|
||||
"testing"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
actions_model "gitea.dev/models/actions"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/timeutil"
|
||||
"gitea.dev/modules/translation"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestViewPullRequestFromRun(t *testing.T) {
|
||||
repo := &repo_model.Repository{ID: 1, OwnerName: "owner", Name: "repo"}
|
||||
|
||||
t.Run("pull ref", func(t *testing.T) {
|
||||
run := &actions_model.ActionRun{Repo: repo, Ref: "refs/pull/123/head"}
|
||||
assert.Equal(t, &ViewPullRequest{Index: "#123", Link: "/owner/repo/pulls/123"}, viewPullRequestFromRun(t.Context(), run, nil))
|
||||
})
|
||||
|
||||
t.Run("pull request event payload", func(t *testing.T) {
|
||||
// a non-pull ref forces the payload branch instead of the ref branch
|
||||
run := &actions_model.ActionRun{Repo: repo, Ref: "refs/heads/feature"}
|
||||
payload := &api.PullRequestPayload{Index: 42}
|
||||
assert.Equal(t, &ViewPullRequest{Index: "#42", Link: "/owner/repo/pulls/42"}, viewPullRequestFromRun(t.Context(), run, payload))
|
||||
})
|
||||
|
||||
t.Run("nil repo", func(t *testing.T) {
|
||||
run := &actions_model.ActionRun{Ref: "refs/pull/1/head"}
|
||||
assert.Nil(t, viewPullRequestFromRun(t.Context(), run, nil))
|
||||
})
|
||||
}
|
||||
|
||||
func TestViewSummaryBranchFromRun(t *testing.T) {
|
||||
repo := &repo_model.Repository{ID: 1, OwnerName: "owner", Name: "repo"}
|
||||
|
||||
t.Run("pull request event same repo", func(t *testing.T) {
|
||||
run := &actions_model.ActionRun{Repo: repo, Ref: "refs/pull/7/head"}
|
||||
payload := &api.PullRequestPayload{
|
||||
PullRequest: &api.PullRequest{Head: &api.PRBranchInfo{
|
||||
Name: "feature",
|
||||
Ref: "refs/heads/feature",
|
||||
RepoID: 1,
|
||||
Repository: &api.Repository{Link: "/owner/repo"},
|
||||
}},
|
||||
}
|
||||
assert.Equal(t, ViewBranch{Name: "feature", Link: "/owner/repo/src/branch/feature"}, viewSummaryBranchFromRun(t.Context(), run, payload))
|
||||
})
|
||||
|
||||
t.Run("pull request event from fork prefixes owner", func(t *testing.T) {
|
||||
run := &actions_model.ActionRun{Repo: repo, Ref: "refs/pull/7/head"}
|
||||
payload := &api.PullRequestPayload{
|
||||
PullRequest: &api.PullRequest{Head: &api.PRBranchInfo{
|
||||
Name: "feature",
|
||||
Ref: "refs/heads/feature",
|
||||
RepoID: 2,
|
||||
Repository: &api.Repository{
|
||||
Link: "/forkowner/repo",
|
||||
Owner: &api.User{UserName: "forkowner"},
|
||||
},
|
||||
}},
|
||||
}
|
||||
assert.Equal(t, ViewBranch{Name: "forkowner:feature", Link: "/forkowner/repo/src/branch/feature"}, viewSummaryBranchFromRun(t.Context(), run, payload))
|
||||
})
|
||||
|
||||
t.Run("push to tag does not query branch", func(t *testing.T) {
|
||||
// a tag ref is not a branch, so no GetBranch DB lookup happens
|
||||
run := &actions_model.ActionRun{Repo: repo, Ref: "refs/tags/v1.0.0"}
|
||||
assert.Equal(t, ViewBranch{Name: "v1.0.0", Link: "/owner/repo/src/tag/v1.0.0"}, viewSummaryBranchFromRun(t.Context(), run, nil))
|
||||
})
|
||||
}
|
||||
|
||||
func TestConvertToViewModel(t *testing.T) {
|
||||
task := &actions_model.ActionTask{
|
||||
Status: actions_model.StatusSuccess,
|
||||
@@ -45,3 +107,79 @@ func TestConvertToViewModel(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, expectedViewJobs, viewJobSteps)
|
||||
}
|
||||
|
||||
func TestConvertToViewModelCancellingTaskDoesNotRenderRunningSteps(t *testing.T) {
|
||||
task := &actions_model.ActionTask{
|
||||
Status: actions_model.StatusCancelling,
|
||||
Steps: []*actions_model.ActionTaskStep{
|
||||
{Name: "Run step-name", Index: 0, Status: actions_model.StatusRunning, LogLength: 1},
|
||||
},
|
||||
}
|
||||
|
||||
viewJobSteps, _, err := convertToViewModel(t.Context(), translation.MockLocale{}, nil, task)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedViewJobs := []*ViewJobStep{
|
||||
{
|
||||
Summary: "Set up job",
|
||||
Duration: "0s",
|
||||
Status: "success",
|
||||
},
|
||||
{
|
||||
Summary: "Run step-name",
|
||||
Duration: "0s",
|
||||
Status: "cancelling",
|
||||
},
|
||||
{
|
||||
Summary: "Complete job",
|
||||
Duration: "0s",
|
||||
Status: "waiting",
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expectedViewJobs, viewJobSteps)
|
||||
}
|
||||
|
||||
func TestPendingNeeds(t *testing.T) {
|
||||
current := &actions_model.ActionRunJob{JobID: "deploy", Needs: []string{"build", "test"}}
|
||||
jobs := []*actions_model.ActionRunJob{
|
||||
current,
|
||||
{JobID: "build", Status: actions_model.StatusSuccess},
|
||||
{JobID: "test", Status: actions_model.StatusRunning},
|
||||
}
|
||||
// "test" is not done yet, "build" succeeded, so only "test" blocks.
|
||||
assert.Equal(t, []string{"test"}, pendingNeeds(current, jobs))
|
||||
|
||||
t.Run("all needs done", func(t *testing.T) {
|
||||
done := []*actions_model.ActionRunJob{
|
||||
current,
|
||||
{JobID: "build", Status: actions_model.StatusSuccess},
|
||||
{JobID: "test", Status: actions_model.StatusSkipped},
|
||||
}
|
||||
assert.Empty(t, pendingNeeds(current, done))
|
||||
})
|
||||
|
||||
t.Run("matrix expansion all required", func(t *testing.T) {
|
||||
matrix := []*actions_model.ActionRunJob{
|
||||
current,
|
||||
{JobID: "build", Status: actions_model.StatusSuccess},
|
||||
{JobID: "build", Status: actions_model.StatusRunning},
|
||||
{JobID: "test", Status: actions_model.StatusSuccess},
|
||||
}
|
||||
assert.Equal(t, []string{"build"}, pendingNeeds(current, matrix))
|
||||
})
|
||||
|
||||
t.Run("unresolved need treated as pending", func(t *testing.T) {
|
||||
missing := []*actions_model.ActionRunJob{current}
|
||||
assert.Equal(t, []string{"build", "test"}, pendingNeeds(current, missing))
|
||||
})
|
||||
|
||||
t.Run("parent job scope", func(t *testing.T) {
|
||||
// a same-named job under a different parent must not satisfy the need
|
||||
scoped := &actions_model.ActionRunJob{JobID: "deploy", Needs: []string{"build"}, ParentJobID: 5}
|
||||
jobs := []*actions_model.ActionRunJob{
|
||||
scoped,
|
||||
{JobID: "build", Status: actions_model.StatusSuccess, ParentJobID: 0},
|
||||
}
|
||||
assert.Equal(t, []string{"build"}, pendingNeeds(scoped, jobs))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
activities_model "gitea.dev/models/activities"
|
||||
git_model "gitea.dev/models/git"
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -48,7 +48,7 @@ func Activity(ctx *context.Context) {
|
||||
ctx.Data["Period"] = period
|
||||
ctx.Data["PeriodText"] = ctx.Tr("repo.activity.period." + period)
|
||||
|
||||
canReadCode := ctx.Repo.CanRead(unit.TypeCode)
|
||||
canReadCode := ctx.Repo.Permission.CanRead(unit.TypeCode)
|
||||
if canReadCode {
|
||||
// GetActivityStats needs to read the default branch to get some information
|
||||
branchExist, _ := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, ctx.Repo.Repository.DefaultBranch)
|
||||
@@ -62,9 +62,9 @@ func Activity(ctx *context.Context) {
|
||||
var err error
|
||||
// TODO: refactor these arguments to a struct
|
||||
ctx.Data["Activity"], err = activities_model.GetActivityStats(ctx, ctx.Repo.Repository, timeFrom,
|
||||
ctx.Repo.CanRead(unit.TypeReleases),
|
||||
ctx.Repo.CanRead(unit.TypeIssues),
|
||||
ctx.Repo.CanRead(unit.TypePullRequests),
|
||||
ctx.Repo.Permission.CanRead(unit.TypeReleases),
|
||||
ctx.Repo.Permission.CanRead(unit.TypeIssues),
|
||||
ctx.Repo.Permission.CanRead(unit.TypePullRequests),
|
||||
canReadCode,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -6,20 +6,20 @@ package repo
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/httpcache"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/services/attachment"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/context/upload"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/modules/httpcache"
|
||||
"gitea.dev/modules/httplib"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/storage"
|
||||
"gitea.dev/services/attachment"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/context/upload"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
func attachmentReadScope(unitType unit.Type) (auth_model.AccessTokenScope, bool) {
|
||||
@@ -143,18 +143,18 @@ func ServeAttachment(ctx *context.Context, uuid string) {
|
||||
return
|
||||
}
|
||||
|
||||
// prevent visiting attachment from other repository directly
|
||||
// The check will be ignored before this code merged.
|
||||
if attach.CreatedUnix > repo_model.LegacyAttachmentMissingRepoIDCutoff && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID != attach.RepoID {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
unitType, repoID, err := repo_service.GetAttachmentLinkedTypeAndRepoID(ctx, attach)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetAttachmentLinkedTypeAndRepoID", err)
|
||||
return
|
||||
}
|
||||
if repoID == 0 {
|
||||
repoID = attach.RepoID
|
||||
}
|
||||
if ctx.Repo.Repository != nil && repoID != 0 && ctx.Repo.Repository.ID != repoID {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if unitType == unit.TypeInvalid { // unlinked attachment can only be accessed by the uploader
|
||||
if !(ctx.IsSigned && attach.UploaderID == ctx.Doer.ID) { // We block if not the uploader
|
||||
@@ -186,6 +186,22 @@ func ServeAttachment(ctx *context.Context, uuid string) {
|
||||
return
|
||||
}
|
||||
|
||||
// Draft release attachments must not be exposed to anyone without write
|
||||
// access, matching the API-side canAccessReleaseDraft gate. Otherwise the
|
||||
// UUID-based web endpoints would leak draft attachments to any recipient of
|
||||
// the (leaked) download URL.
|
||||
if unitType == unit.TypeReleases && attach.ReleaseID != 0 && !perm.CanWrite(unit.TypeReleases) {
|
||||
rel, err := repo_model.GetReleaseByID(ctx, attach.ReleaseID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetReleaseByID", err)
|
||||
return
|
||||
}
|
||||
if rel.IsDraft {
|
||||
ctx.HTTPError(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if requiredScope, ok := attachmentReadScope(unitType); ok {
|
||||
context.CheckTokenScopes(ctx, repo, requiredScope)
|
||||
if ctx.Written() {
|
||||
|
||||
@@ -12,30 +12,31 @@ import (
|
||||
"path"
|
||||
"strconv"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/charset"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/languagestats"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/highlight"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"gitea.dev/models/gituser"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/charset"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/languagestats"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/highlight"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
type blameRow struct {
|
||||
RowNumber int
|
||||
|
||||
Avatar template.HTML
|
||||
PreviousSha string
|
||||
PreviousShaURL string
|
||||
CommitURL string
|
||||
CommitMessage string
|
||||
CommitSince template.HTML
|
||||
|
||||
AvatarStackData *gituser.AvatarStackData
|
||||
|
||||
Code template.HTML
|
||||
EscapeStatus *charset.EscapeStatus
|
||||
}
|
||||
@@ -174,9 +175,9 @@ func fillBlameResult(br *gitrepo.BlameReader, r *blameResult) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func processBlameParts(ctx *context.Context, blameParts []*gitrepo.BlamePart) map[string]*user_model.UserCommit {
|
||||
func processBlameParts(ctx *context.Context, blameParts []*gitrepo.BlamePart) map[string]*gituser.UserCommit {
|
||||
// store commit data by SHA to look up avatar info etc
|
||||
commitNames := make(map[string]*user_model.UserCommit)
|
||||
commitNames := make(map[string]*gituser.UserCommit)
|
||||
// and as blameParts can reference the same commits multiple
|
||||
// times, we cache the lookup work locally
|
||||
commits := make([]*git.Commit, 0, len(blameParts))
|
||||
@@ -209,33 +210,28 @@ func processBlameParts(ctx *context.Context, blameParts []*gitrepo.BlamePart) ma
|
||||
}
|
||||
|
||||
// populate commit email addresses to later look up avatars.
|
||||
validatedCommits, err := user_model.ValidateCommitsWithEmails(ctx, commits)
|
||||
userCommits, err := gituser.GetUserCommitsByGitCommits(ctx, commits, ctx.Repo.RepoLink, ctx.Repo.RefFullName)
|
||||
if err != nil {
|
||||
ctx.ServerError("ValidateCommitsWithEmails", err)
|
||||
ctx.ServerError("GetUserCommitsByGitCommits", err)
|
||||
return nil
|
||||
}
|
||||
for _, c := range validatedCommits {
|
||||
commitNames[c.ID.String()] = c
|
||||
for _, c := range userCommits {
|
||||
commitNames[c.GitCommit.ID.String()] = c
|
||||
}
|
||||
|
||||
return commitNames
|
||||
}
|
||||
|
||||
func renderBlameFillFirstBlameRow(repoLink string, avatarUtils *templates.AvatarUtils, part *gitrepo.BlamePart, commit *user_model.UserCommit, br *blameRow) {
|
||||
if commit.User != nil {
|
||||
br.Avatar = avatarUtils.Avatar(commit.User, 18)
|
||||
} else {
|
||||
br.Avatar = avatarUtils.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18)
|
||||
}
|
||||
|
||||
func renderBlameFillFirstBlameRow(ctx *context.Context, repoLink string, part *gitrepo.BlamePart, commit *gituser.UserCommit, br *blameRow) {
|
||||
br.AvatarStackData = gituser.BuildAvatarStackData(ctx, commit.GitCommit.AllAuthorIdentities(), nil)
|
||||
br.PreviousSha = part.PreviousSha
|
||||
br.PreviousShaURL = fmt.Sprintf("%s/blame/commit/%s/%s", repoLink, url.PathEscape(part.PreviousSha), util.PathEscapeSegments(part.PreviousPath))
|
||||
br.CommitURL = fmt.Sprintf("%s/commit/%s", repoLink, url.PathEscape(part.Sha))
|
||||
br.CommitMessage = commit.Message()
|
||||
br.CommitSince = templates.TimeSince(commit.Author.When)
|
||||
br.CommitMessage = commit.GitCommit.MessageUTF8()
|
||||
br.CommitSince = templates.TimeSince(commit.GitCommit.Author.When)
|
||||
}
|
||||
|
||||
func renderBlame(ctx *context.Context, blameParts []*gitrepo.BlamePart, commitNames map[string]*user_model.UserCommit) {
|
||||
func renderBlame(ctx *context.Context, blameParts []*gitrepo.BlamePart, commitNames map[string]*gituser.UserCommit) {
|
||||
language, err := languagestats.GetFileLanguage(ctx, ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath)
|
||||
if err != nil {
|
||||
log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
|
||||
@@ -243,7 +239,6 @@ func renderBlame(ctx *context.Context, blameParts []*gitrepo.BlamePart, commitNa
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
rows := make([]*blameRow, 0)
|
||||
avatarUtils := templates.NewAvatarUtils(ctx)
|
||||
rowNumber := 0 // will be 1-based
|
||||
for _, part := range blameParts {
|
||||
for partLineIdx, line := range part.Lines {
|
||||
@@ -258,7 +253,7 @@ func renderBlame(ctx *context.Context, blameParts []*gitrepo.BlamePart, commitNa
|
||||
}
|
||||
|
||||
if partLineIdx == 0 {
|
||||
renderBlameFillFirstBlameRow(ctx.Repo.RepoLink, avatarUtils, part, commitNames[part.Sha], br)
|
||||
renderBlameFillFirstBlameRow(ctx, ctx.Repo.RepoLink, part, commitNames[part.Sha], br)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,24 +11,24 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
release_service "code.gitea.io/gitea/services/release"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
git_model "gitea.dev/models/git"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/optional"
|
||||
repo_module "gitea.dev/modules/repository"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/routers/utils"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
pull_service "gitea.dev/services/pull"
|
||||
release_service "gitea.dev/services/release"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -39,10 +39,10 @@ const (
|
||||
func Branches(ctx *context.Context) {
|
||||
ctx.Data["Title"] = "Branches"
|
||||
ctx.Data["AllowsPulls"] = ctx.Repo.Repository.AllowsPulls(ctx)
|
||||
ctx.Data["IsWriter"] = ctx.Repo.CanWrite(unit.TypeCode)
|
||||
ctx.Data["IsWriter"] = ctx.Repo.Permission.CanWrite(unit.TypeCode)
|
||||
ctx.Data["IsMirror"] = ctx.Repo.Repository.IsMirror
|
||||
// TODO: Can be replaced by ctx.Repo.PullRequestCtx.CanCreateNewPull()
|
||||
ctx.Data["CanPull"] = ctx.Repo.CanWrite(unit.TypeCode) ||
|
||||
ctx.Data["CanPull"] = ctx.Repo.Permission.CanWrite(unit.TypeCode) ||
|
||||
(ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID))
|
||||
ctx.Data["PageIsViewCode"] = true
|
||||
ctx.Data["PageIsBranches"] = true
|
||||
@@ -52,13 +52,16 @@ func Branches(ctx *context.Context) {
|
||||
|
||||
kw := ctx.FormString("q")
|
||||
|
||||
defaultBranch, branches, branchesCount, err := repo_service.LoadBranches(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, optional.None[bool](), kw, page, pageSize)
|
||||
defaultBranchOptional, branches, branchesCount, err := repo_service.LoadBranches(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, optional.None[bool](), kw, page, pageSize)
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadBranches", err)
|
||||
return
|
||||
}
|
||||
|
||||
commitIDs := []string{defaultBranch.DBBranch.CommitID}
|
||||
commitIDs := make([]string, 0, len(branches)+1)
|
||||
if defaultBranchOptional != nil {
|
||||
commitIDs = append(commitIDs, defaultBranchOptional.DBBranch.CommitID)
|
||||
}
|
||||
for _, branch := range branches {
|
||||
commitIDs = append(commitIDs, branch.DBBranch.CommitID)
|
||||
}
|
||||
@@ -68,7 +71,7 @@ func Branches(ctx *context.Context) {
|
||||
ctx.ServerError("LoadBranches", err)
|
||||
return
|
||||
}
|
||||
if !ctx.Repo.CanRead(unit.TypeActions) {
|
||||
if !ctx.Repo.Permission.CanRead(unit.TypeActions) {
|
||||
for key := range commitStatuses {
|
||||
git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key])
|
||||
}
|
||||
@@ -83,7 +86,7 @@ func Branches(ctx *context.Context) {
|
||||
ctx.Data["Branches"] = branches
|
||||
ctx.Data["CommitStatus"] = commitStatus
|
||||
ctx.Data["CommitStatuses"] = commitStatuses
|
||||
ctx.Data["DefaultBranchBranch"] = defaultBranch
|
||||
ctx.Data["DefaultBranchBranch"] = defaultBranchOptional
|
||||
pager := context.NewPagination(branchesCount, pageSize, page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
@@ -152,7 +155,7 @@ func RestoreBranchPost(ctx *context.Context) {
|
||||
objectFormat := git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName)
|
||||
|
||||
// Don't return error below this
|
||||
if err := repo_service.PushUpdate(
|
||||
if err := repo_service.PushUpdates(
|
||||
&repo_module.PushUpdateOptions{
|
||||
RefFullName: git.RefNameFromBranch(deletedBranch.Name),
|
||||
OldCommitID: objectFormat.EmptyObjectID().String(),
|
||||
@@ -231,7 +234,7 @@ func CreateBranch(ctx *context.Context) {
|
||||
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
|
||||
"Message": ctx.Tr("repo.editor.push_rejected"),
|
||||
"Summary": ctx.Tr("repo.editor.push_rejected_summary"),
|
||||
"Details": utils.SanitizeFlashErrorString(e.Message),
|
||||
"Details": utils.EscapeFlashErrorString(e.Message),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("UpdatePullRequest.HTMLString", err)
|
||||
@@ -261,7 +264,7 @@ func MergeUpstream(ctx *context.Context) {
|
||||
branchName := ctx.FormString("branch")
|
||||
_, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName, false)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrPermissionDenied) {
|
||||
ctx.JSONErrorNotFound()
|
||||
return
|
||||
} else if pull_service.IsErrMergeConflicts(err) {
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
contributors_service "code.gitea.io/gitea/services/repository"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/context"
|
||||
contributors_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -9,33 +9,33 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/renderhelper"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
unit_model "code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/charset"
|
||||
"code.gitea.io/gitea/modules/fileicon"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
git_service "code.gitea.io/gitea/services/git"
|
||||
"code.gitea.io/gitea/services/gitdiff"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
"code.gitea.io/gitea/services/repository/gitgraph"
|
||||
asymkey_model "gitea.dev/models/asymkey"
|
||||
"gitea.dev/models/db"
|
||||
git_model "gitea.dev/models/git"
|
||||
"gitea.dev/models/gituser"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/models/renderhelper"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
unit_model "gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/base"
|
||||
"gitea.dev/modules/charset"
|
||||
"gitea.dev/modules/fileicon"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/markup"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
asymkey_service "gitea.dev/services/asymkey"
|
||||
"gitea.dev/services/context"
|
||||
git_service "gitea.dev/services/git"
|
||||
"gitea.dev/services/gitdiff"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
"gitea.dev/services/repository/gitgraph"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -50,7 +50,7 @@ func RefCommits(ctx *context.Context) {
|
||||
switch {
|
||||
case len(ctx.Repo.TreePath) == 0:
|
||||
Commits(ctx)
|
||||
case ctx.Repo.TreePath == "search":
|
||||
case ctx.Repo.TreePath == "search": // FIXME: legacy dirty design, it conflicts with the FileHistory
|
||||
SearchCommits(ctx)
|
||||
default:
|
||||
FileHistory(ctx)
|
||||
@@ -97,8 +97,6 @@ func Commits(ctx *context.Context) {
|
||||
} else {
|
||||
ctx.Data["CommitsTagsMap"] = commitsTagsMap
|
||||
}
|
||||
ctx.Data["Username"] = ctx.Repo.Owner.Name
|
||||
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
|
||||
ctx.Data["CommitCount"] = commitsCount
|
||||
|
||||
pager := context.NewPagination(commitsCount, pageSize, page, 5)
|
||||
@@ -164,9 +162,6 @@ func Graph(ctx *context.Context) {
|
||||
|
||||
ctx.Data["AllRefs"] = gitRefs
|
||||
|
||||
ctx.Data["Username"] = ctx.Repo.Owner.Name
|
||||
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
|
||||
|
||||
divOnly := ctx.FormBool("div-only")
|
||||
queryParams := ctx.Req.URL.Query()
|
||||
queryParams.Del("div-only")
|
||||
@@ -210,8 +205,6 @@ func SearchCommits(ctx *context.Context) {
|
||||
if all {
|
||||
ctx.Data["All"] = true
|
||||
}
|
||||
ctx.Data["Username"] = ctx.Repo.Owner.Name
|
||||
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
|
||||
ctx.HTML(http.StatusOK, tplCommits)
|
||||
}
|
||||
|
||||
@@ -222,39 +215,52 @@ func FileHistory(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
commitsCount, err := gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository, ctx.Repo.RefFullName.ShortName(), ctx.Repo.TreePath)
|
||||
if err != nil {
|
||||
ctx.ServerError("FileCommitsCount", err)
|
||||
return
|
||||
} else if commitsCount == 0 {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
followRename := ctx.FormBool("follow-rename")
|
||||
ctx.Data["ShowFollowRename"] = true
|
||||
ctx.Data["FollowRenameChecked"] = followRename
|
||||
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
|
||||
commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange(
|
||||
commits, hasMore, err := ctx.Repo.GitRepo.CommitsByFileAndRange(
|
||||
git.CommitsByFileAndRangeOptions{
|
||||
Revision: ctx.Repo.RefFullName.ShortName(), // FIXME: legacy code used ShortName
|
||||
File: ctx.Repo.TreePath,
|
||||
Page: page,
|
||||
Revision: ctx.Repo.RefFullName.ShortName(), // FIXME: legacy code used ShortName
|
||||
File: ctx.Repo.TreePath,
|
||||
Page: page,
|
||||
FollowRename: followRename,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("CommitsByFileAndRange", err)
|
||||
return
|
||||
}
|
||||
|
||||
var commitsCount int64
|
||||
if followRename {
|
||||
// there is no quick method to know the total count when "follow rename"
|
||||
commitsCount = -1
|
||||
} else {
|
||||
commitsCount, err = gitrepo.FileCommitsCount(ctx, ctx.Repo.Repository, ctx.Repo.RefFullName.ShortName(), ctx.Repo.TreePath)
|
||||
if err != nil {
|
||||
ctx.ServerError("FileCommitsCount", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(commits) == 0 {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["FileTreePath"] = ctx.Repo.TreePath
|
||||
ctx.Data["CommitCount"] = commitsCount
|
||||
ctx.Data["Commits"], err = processGitCommits(ctx, commits)
|
||||
if err != nil {
|
||||
ctx.ServerError("processGitCommits", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Username"] = ctx.Repo.Owner.Name
|
||||
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
|
||||
ctx.Data["FileTreePath"] = ctx.Repo.TreePath
|
||||
ctx.Data["CommitCount"] = commitsCount
|
||||
|
||||
pager := context.NewPagination(commitsCount, setting.Git.CommitsRangeSize, page, 5)
|
||||
if commitsCount == -1 {
|
||||
pager.WithUnlimitedPaging(len(commits), hasMore)
|
||||
}
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
ctx.HTML(http.StatusOK, tplCommits)
|
||||
@@ -322,7 +328,7 @@ func Diff(ctx *context.Context) {
|
||||
MaxLines: maxLines,
|
||||
MaxLineCharacters: setting.Git.MaxGitDiffLineCharacters,
|
||||
MaxFiles: maxFiles,
|
||||
WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)),
|
||||
WhitespaceBehavior: gitdiff.GetWhitespaceFlag(GetWhitespaceBehavior(ctx)),
|
||||
}, files...)
|
||||
if err != nil {
|
||||
ctx.NotFound(err)
|
||||
@@ -347,8 +353,6 @@ func Diff(ctx *context.Context) {
|
||||
|
||||
ctx.Data["CommitID"] = commitID
|
||||
ctx.Data["AfterCommitID"] = commitID
|
||||
ctx.Data["Username"] = userName
|
||||
ctx.Data["Reponame"] = repoName
|
||||
|
||||
var parentCommit *git.Commit
|
||||
var parentCommitID string
|
||||
@@ -361,7 +365,7 @@ func Diff(ctx *context.Context) {
|
||||
parentCommitID = parentCommit.ID.String()
|
||||
}
|
||||
setCompareContext(ctx, parentCommit, commit, userName, repoName)
|
||||
ctx.Data["Title"] = commit.Summary() + " · " + base.ShortSha(commitID)
|
||||
ctx.Data["Title"] = commit.MessageTitle() + " · " + base.ShortSha(commitID)
|
||||
ctx.Data["Commit"] = commit
|
||||
ctx.Data["Diff"] = diff
|
||||
ctx.Data["DiffBlobExcerptData"] = diffBlobExcerptData
|
||||
@@ -384,7 +388,7 @@ func Diff(ctx *context.Context) {
|
||||
if err != nil {
|
||||
log.Error("GetLatestCommitStatus: %v", err)
|
||||
}
|
||||
if !ctx.Repo.CanRead(unit_model.TypeActions) {
|
||||
if !ctx.Repo.Permission.CanRead(unit_model.TypeActions) {
|
||||
git_model.CommitStatusesHideActionsURL(ctx, statuses)
|
||||
}
|
||||
|
||||
@@ -393,7 +397,8 @@ func Diff(ctx *context.Context) {
|
||||
|
||||
verification := asymkey_service.ParseCommitWithSignature(ctx, commit)
|
||||
ctx.Data["Verification"] = verification
|
||||
ctx.Data["Author"] = user_model.ValidateCommitWithEmail(ctx, commit)
|
||||
ctx.Data["Author"] = user_model.GetUserByGitAuthor(ctx, commit)
|
||||
ctx.Data["CommitOtherParticipants"] = gituser.BuildAvatarStackData(ctx, commit.CoAuthorIdentities(), nil).Participants
|
||||
ctx.Data["Parents"] = parents
|
||||
ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0
|
||||
|
||||
@@ -408,13 +413,10 @@ func Diff(ctx *context.Context) {
|
||||
err = git.GetNote(ctx, ctx.Repo.GitRepo, commitID, note)
|
||||
if err == nil {
|
||||
ctx.Data["NoteCommit"] = note.Commit
|
||||
ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit)
|
||||
rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{CurrentRefPath: path.Join("commit", util.PathEscapeSegments(commitID))})
|
||||
ctx.Data["NoteRendered"], err = markup.PostProcessCommitMessage(rctx, template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{}))))
|
||||
if err != nil {
|
||||
ctx.ServerError("PostProcessCommitMessage", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["NoteAuthor"] = user_model.GetUserByGitAuthor(ctx, note.Commit)
|
||||
rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{CurrentRefSubURL: "commit/" + util.PathEscapeSegments(commitID)})
|
||||
htmlMessage := template.HTML(template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{}))))
|
||||
ctx.Data["NoteRendered"] = markup.PostProcessCommitMessage(rctx, htmlMessage)
|
||||
} else if !git.IsErrNotExist(err) {
|
||||
log.Error("GetNote: %v", err)
|
||||
}
|
||||
@@ -461,11 +463,11 @@ func RawDiff(ctx *context.Context) {
|
||||
}
|
||||
|
||||
func processGitCommits(ctx *context.Context, gitCommits []*git.Commit) ([]*git_model.SignCommitWithStatuses, error) {
|
||||
commits, err := git_service.ConvertFromGitCommit(ctx, gitCommits, ctx.Repo.Repository)
|
||||
commits, err := git_service.ConvertFromGitCommit(ctx, gitCommits, ctx.Repo.Repository, ctx.Repo.RefFullName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ctx.Repo.CanRead(unit_model.TypeActions) {
|
||||
if !ctx.Repo.Permission.CanRead(unit_model.TypeActions) {
|
||||
for _, commit := range commits {
|
||||
if commit.Status == nil {
|
||||
continue
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
unit_model "code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
git_model "gitea.dev/models/git"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
unit_model "gitea.dev/models/unit"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/services/context"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
type RecentBranchesPromptDataStruct struct {
|
||||
|
||||
@@ -15,34 +15,34 @@ import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/charset"
|
||||
csv_module "code.gitea.io/gitea/modules/csv"
|
||||
"code.gitea.io/gitea/modules/fileicon"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/typesniffer"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/common"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/context/upload"
|
||||
git_service "code.gitea.io/gitea/services/git"
|
||||
"code.gitea.io/gitea/services/gitdiff"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
"gitea.dev/models/db"
|
||||
git_model "gitea.dev/models/git"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
access_model "gitea.dev/models/perm/access"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unit"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/base"
|
||||
"gitea.dev/modules/charset"
|
||||
csv_module "gitea.dev/modules/csv"
|
||||
"gitea.dev/modules/fileicon"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/markup"
|
||||
"gitea.dev/modules/optional"
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/typesniffer"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/routers/common"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/context/upload"
|
||||
git_service "gitea.dev/services/git"
|
||||
"gitea.dev/services/gitdiff"
|
||||
user_service "gitea.dev/services/user"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -190,57 +190,44 @@ func setCsvCompareContext(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// ParseCompareInfo parse compare info between two commit for preparing comparing references
|
||||
func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo {
|
||||
type comparePageInfoType struct {
|
||||
compareInfo *git_service.CompareInfo
|
||||
nothingToCompare bool
|
||||
allowCreatePull bool
|
||||
}
|
||||
|
||||
func newComparePageInfo() *comparePageInfoType {
|
||||
return &comparePageInfoType{}
|
||||
}
|
||||
|
||||
// parseCompareInfo parse compare info between two commit for preparing comparing references
|
||||
func (cpi *comparePageInfoType) parseCompareInfo(ctx *context.Context, compareParam string) error {
|
||||
baseRepo := ctx.Repo.Repository
|
||||
fileOnly := ctx.FormBool("file-only")
|
||||
|
||||
// 1 Parse compare router param
|
||||
compareReq := common.ParseCompareRouterParam(ctx.PathParam("*"))
|
||||
|
||||
// remove the check when we support compare with carets
|
||||
if compareReq.BaseOriRefSuffix != "" {
|
||||
ctx.HTTPError(http.StatusBadRequest, "Unsupported comparison syntax: ref with suffix")
|
||||
return nil
|
||||
}
|
||||
compareReq := common.ParseCompareRouterParam(compareParam)
|
||||
|
||||
// 2 get repository and owner for head
|
||||
headOwner, headRepo, err := common.GetHeadOwnerAndRepo(ctx, baseRepo, compareReq)
|
||||
switch {
|
||||
case errors.Is(err, util.ErrInvalidArgument):
|
||||
ctx.HTTPError(http.StatusBadRequest, err.Error())
|
||||
return nil
|
||||
case errors.Is(err, util.ErrNotExist):
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
case err != nil:
|
||||
ctx.ServerError("GetHeadOwnerAndRepo", err)
|
||||
return nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
isSameRepo := baseRepo.ID == headRepo.ID
|
||||
|
||||
// 3 permission check
|
||||
// base repository's code unit read permission check has been done on web.go
|
||||
permBase := ctx.Repo.Permission
|
||||
|
||||
// If we're not merging from the same repo:
|
||||
isSameRepo := baseRepo.ID == headRepo.ID
|
||||
if !isSameRepo {
|
||||
// Assert ctx.Doer has permission to read headRepo's codes
|
||||
permHead, err := access_model.GetDoerRepoPermission(ctx, headRepo, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDoerRepoPermission", err)
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
if !permHead.CanRead(unit.TypeCode) {
|
||||
if log.IsTrace() {
|
||||
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v",
|
||||
ctx.Doer,
|
||||
headRepo,
|
||||
permHead)
|
||||
}
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
return util.NewNotExistErrorf("") // permission: no error message for end users
|
||||
}
|
||||
ctx.Data["CanWriteToHeadRepo"] = permHead.CanWrite(unit.TypeCode)
|
||||
}
|
||||
@@ -249,26 +236,18 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo {
|
||||
baseRefName := util.IfZero(compareReq.BaseOriRef, baseRepo.GetPullRequestTargetBranch(ctx))
|
||||
headRefName := util.IfZero(compareReq.HeadOriRef, headRepo.DefaultBranch)
|
||||
|
||||
baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(baseRefName)
|
||||
if baseRef == "" {
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
baseRef, err := common.ResolveRefWithSuffix(ctx.Repo.GitRepo, baseRefName, compareReq.BaseOriRefSuffix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var headGitRepo *git.Repository
|
||||
if isSameRepo {
|
||||
headGitRepo = ctx.Repo.GitRepo
|
||||
} else {
|
||||
headGitRepo, err = gitrepo.OpenRepository(ctx, headRepo)
|
||||
if err != nil {
|
||||
ctx.ServerError("OpenRepository", err)
|
||||
return nil
|
||||
}
|
||||
defer headGitRepo.Close()
|
||||
headGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, headRepo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
headRef := headGitRepo.UnstableGuessRefByShortName(headRefName)
|
||||
if headRef == "" {
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
|
||||
headRef, err := common.ResolveRefWithSuffix(headGitRepo, headRefName, compareReq.HeadOriRefSuffix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.Data["BaseName"] = baseRepo.OwnerName
|
||||
@@ -292,12 +271,9 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo {
|
||||
var rootRepo *repo_model.Repository
|
||||
if baseRepo.IsFork {
|
||||
err = baseRepo.GetBaseRepo(ctx)
|
||||
if err != nil {
|
||||
if !repo_model.IsErrRepoNotExist(err) {
|
||||
ctx.ServerError("Unable to find root repo", err)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
if err != nil && !repo_model.IsErrRepoNotExist(err) {
|
||||
return err
|
||||
} else if err == nil {
|
||||
rootRepo = baseRepo.BaseRepo
|
||||
}
|
||||
}
|
||||
@@ -314,42 +290,10 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo {
|
||||
}
|
||||
}
|
||||
|
||||
has := headRepo != nil
|
||||
// 3. If the base is a forked from "RootRepo" and the owner of
|
||||
// the "RootRepo" is the :headUser - set headRepo to that
|
||||
if !has && rootRepo != nil && rootRepo.OwnerID == headOwner.ID {
|
||||
headRepo = rootRepo
|
||||
has = true
|
||||
}
|
||||
|
||||
// 4. If the ctx.Doer has their own fork of the baseRepo and the headUser is the ctx.Doer
|
||||
// set the headRepo to the ownFork
|
||||
if !has && ownForkRepo != nil && ownForkRepo.OwnerID == headOwner.ID {
|
||||
headRepo = ownForkRepo
|
||||
has = true
|
||||
}
|
||||
|
||||
// 5. If the headOwner has a fork of the baseRepo - use that
|
||||
if !has {
|
||||
headRepo = repo_model.GetForkedRepo(ctx, headOwner.ID, baseRepo.ID)
|
||||
has = headRepo != nil
|
||||
}
|
||||
|
||||
// 6. If the baseRepo is a fork and the headUser has a fork of that use that
|
||||
if !has && baseRepo.IsFork {
|
||||
headRepo = repo_model.GetForkedRepo(ctx, headOwner.ID, baseRepo.ForkID)
|
||||
has = headRepo != nil
|
||||
}
|
||||
|
||||
// 7. Otherwise if we're not the same repo and haven't found a repo give up
|
||||
if !isSameRepo && !has {
|
||||
ctx.Data["PageIsComparePull"] = false
|
||||
}
|
||||
|
||||
ctx.Data["HeadRepo"] = headRepo
|
||||
ctx.Data["BaseCompareRepo"] = ctx.Repo.Repository
|
||||
|
||||
// If we have a rootRepo and it's different from:
|
||||
// If we have a rootRepo, and it's different from:
|
||||
// 1. the computed base
|
||||
// 2. the computed head
|
||||
// then get the branches of it
|
||||
@@ -362,17 +306,15 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo {
|
||||
if !fileOnly {
|
||||
branches, tags, err := getBranchesAndTagsForRepo(ctx, rootRepo)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetBranchesForRepo", err)
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.Data["RootRepoBranches"] = branches
|
||||
ctx.Data["RootRepoTags"] = tags
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a ownForkRepo and it's different from:
|
||||
// If we have a ownForkRepo, and it's different from:
|
||||
// 1. The computed base
|
||||
// 2. The computed head
|
||||
// 3. The rootRepo (if we have one)
|
||||
@@ -387,8 +329,7 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo {
|
||||
if !fileOnly {
|
||||
branches, tags, err := getBranchesAndTagsForRepo(ctx, ownForkRepo)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetBranchesForRepo", err)
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
ctx.Data["OwnForkRepoBranches"] = branches
|
||||
ctx.Data["OwnForkRepoTags"] = tags
|
||||
@@ -396,38 +337,16 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// Treat as pull request if both references are branches
|
||||
if ctx.Data["PageIsComparePull"] == nil {
|
||||
ctx.Data["PageIsComparePull"] = baseRef.IsBranch() && headRef.IsBranch() && permBase.CanReadIssuesOrPulls(true)
|
||||
}
|
||||
|
||||
if ctx.Data["PageIsComparePull"] == true && !permBase.CanReadIssuesOrPulls(true) {
|
||||
if log.IsTrace() {
|
||||
log.Trace("Permission Denied: User: %-v cannot create/read pull requests in Repo: %-v\nUser in baseRepo has Permissions: %-+v",
|
||||
ctx.Doer,
|
||||
baseRepo,
|
||||
permBase)
|
||||
}
|
||||
ctx.NotFound(nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
compareInfo, err := git_service.GetCompareInfo(ctx, baseRepo, headRepo, headGitRepo, baseRef, headRef, compareReq.DirectComparison(), fileOnly)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.Data["IsNoMergeBase"] = true
|
||||
return compareInfo
|
||||
}
|
||||
ctx.ServerError("GetCompareInfo", err)
|
||||
return nil
|
||||
}
|
||||
if compareReq.DirectComparison() {
|
||||
ctx.Data["BeforeCommitID"] = compareInfo.BaseCommitID
|
||||
} else {
|
||||
ctx.Data["BeforeCommitID"] = compareInfo.MergeBase
|
||||
return err
|
||||
}
|
||||
|
||||
return compareInfo
|
||||
// Treat as pull request if both references are branches
|
||||
cpi.allowCreatePull = baseRef.IsBranch() && headRef.IsBranch() && permBase.CanReadIssuesOrPulls(true)
|
||||
cpi.allowCreatePull = cpi.allowCreatePull && compareInfo.CompareBase != ""
|
||||
cpi.compareInfo = &compareInfo
|
||||
return nil
|
||||
}
|
||||
|
||||
// autoTitleFromBranchName humanizes a branch name into a PR title.
|
||||
@@ -467,16 +386,14 @@ func prepareNewPullRequestTitleContent(ci *git_service.CompareInfo, commits []*g
|
||||
if useFirstCommitAsTitle {
|
||||
// the "commits" are from "ShowPrettyFormatLogToList", which is ordered from newest to oldest, here take the oldest one
|
||||
c := commits[len(commits)-1]
|
||||
title = strings.TrimSpace(c.UserCommit.Summary())
|
||||
title = c.UserCommit.GitCommit.MessageTitle()
|
||||
} else {
|
||||
title = autoTitleFromBranchName(ci.HeadRef.ShortName())
|
||||
}
|
||||
|
||||
if len(commits) == 1 {
|
||||
c := commits[0]
|
||||
_, content, _ = strings.Cut(strings.TrimSpace(c.UserCommit.CommitMessage), "\n")
|
||||
content = strings.TrimSpace(content)
|
||||
content = string(charset.ToUTF8([]byte(content), charset.ConvertOpts{}))
|
||||
content = c.GitCommit.MessageBody()
|
||||
}
|
||||
|
||||
var titleTrailer string
|
||||
@@ -492,16 +409,18 @@ func prepareNewPullRequestTitleContent(ci *git_service.CompareInfo, commits []*g
|
||||
return title, content
|
||||
}
|
||||
|
||||
// PrepareCompareDiff renders compare diff page
|
||||
func PrepareCompareDiff(
|
||||
ctx *context.Context,
|
||||
ci *git_service.CompareInfo,
|
||||
whitespaceBehavior gitcmd.TrustedCmdArgs,
|
||||
) (nothingToCompare bool) {
|
||||
// prepareCompareDiff renders compare diff page. TODO: need to refactor it and other "compare diff" related functions together
|
||||
func (cpi *comparePageInfoType) prepareCompareDiff(ctx *context.Context, whitespaceBehavior gitcmd.TrustedCmdArgs) {
|
||||
ci := cpi.compareInfo
|
||||
if ci.CompareBase == "" {
|
||||
cpi.nothingToCompare = true
|
||||
return
|
||||
}
|
||||
repo := ctx.Repo.Repository
|
||||
headCommitID := ci.HeadCommitID
|
||||
|
||||
ctx.Data["CommitRepoLink"] = ci.HeadRepo.Link()
|
||||
ctx.Data["BeforeCommitID"] = ci.CompareBase
|
||||
ctx.Data["AfterCommitID"] = headCommitID
|
||||
|
||||
// follow GitHub's behavior: autofill the form and expand
|
||||
@@ -511,26 +430,18 @@ func PrepareCompareDiff(
|
||||
ctx.Data["TitleQuery"] = newPrFormTitle
|
||||
ctx.Data["BodyQuery"] = newPrFormBody
|
||||
|
||||
if (headCommitID == ci.MergeBase && !ci.DirectComparison()) ||
|
||||
headCommitID == ci.BaseCommitID {
|
||||
ctx.Data["IsNothingToCompare"] = true
|
||||
if unit, err := repo.GetUnit(ctx, unit.TypePullRequests); err == nil {
|
||||
config := unit.PullRequestsConfig()
|
||||
if headCommitID == ci.CompareBase {
|
||||
config := repo.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig()
|
||||
// if auto-detect manual merge, an empty PR will be closed immediately because it is already on base branch
|
||||
supportEmptyPr := !config.AutodetectManualMerge
|
||||
acrossRepoPr := !ci.IsSameRef()
|
||||
ctx.Data["AllowEmptyPr"] = supportEmptyPr && acrossRepoPr
|
||||
|
||||
if !config.AutodetectManualMerge {
|
||||
ctx.Data["AllowEmptyPr"] = !ci.IsSameRef()
|
||||
return ci.IsSameRef()
|
||||
}
|
||||
|
||||
ctx.Data["AllowEmptyPr"] = false
|
||||
}
|
||||
return true
|
||||
cpi.nothingToCompare = true
|
||||
return
|
||||
}
|
||||
|
||||
beforeCommitID := ci.MergeBase
|
||||
if ci.DirectComparison() {
|
||||
beforeCommitID = ci.BaseCommitID
|
||||
}
|
||||
beforeCommitID := ci.CompareBase
|
||||
|
||||
maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles
|
||||
files := ctx.FormStrings("files")
|
||||
@@ -553,12 +464,12 @@ func PrepareCompareDiff(
|
||||
}, ctx.FormStrings("files")...)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDiff", err)
|
||||
return false
|
||||
return
|
||||
}
|
||||
diffShortStat, err := gitdiff.GetDiffShortStat(ctx, ci.HeadRepo, ci.HeadGitRepo, beforeCommitID, headCommitID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDiffShortStat", err)
|
||||
return false
|
||||
return
|
||||
}
|
||||
ctx.Data["DiffShortStat"] = diffShortStat
|
||||
ctx.Data["Diff"] = diff
|
||||
@@ -573,7 +484,7 @@ func PrepareCompareDiff(
|
||||
diffTree, err := gitdiff.GetDiffTree(ctx, ci.HeadGitRepo, false, beforeCommitID, headCommitID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetDiffTree", err)
|
||||
return false
|
||||
return
|
||||
}
|
||||
|
||||
renderedIconPool := fileicon.NewRenderedIconPool()
|
||||
@@ -586,7 +497,7 @@ func PrepareCompareDiff(
|
||||
headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetCommit", err)
|
||||
return false
|
||||
return
|
||||
}
|
||||
|
||||
baseGitRepo := ctx.Repo.GitRepo
|
||||
@@ -594,24 +505,20 @@ func PrepareCompareDiff(
|
||||
beforeCommit, err := baseGitRepo.GetCommit(beforeCommitID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetCommit", err)
|
||||
return false
|
||||
return
|
||||
}
|
||||
|
||||
commits, err := processGitCommits(ctx, ci.Commits)
|
||||
if err != nil {
|
||||
ctx.ServerError("processGitCommits", err)
|
||||
return false
|
||||
return
|
||||
}
|
||||
ctx.Data["Commits"] = commits
|
||||
ctx.Data["CommitCount"] = len(commits)
|
||||
|
||||
ctx.Data["title"], ctx.Data["content"] = prepareNewPullRequestTitleContent(ci, commits, setting.Repository.PullRequest.DefaultTitleSource)
|
||||
ctx.Data["Username"] = ci.HeadRepo.OwnerName
|
||||
ctx.Data["Reponame"] = ci.HeadRepo.Name
|
||||
|
||||
setCompareContext(ctx, beforeCommit, headCommit, ci.HeadRepo.OwnerName, repo.Name)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repository) (branches, tags []string, err error) {
|
||||
@@ -632,27 +539,24 @@ func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repositor
|
||||
|
||||
// CompareDiff show different from one commit to another commit
|
||||
func CompareDiff(ctx *context.Context) {
|
||||
ci := ParseCompareInfo(ctx)
|
||||
if ctx.Written() {
|
||||
comparePageInfo := newComparePageInfo()
|
||||
err := comparePageInfo.parseCompareInfo(ctx, ctx.PathParam("*"))
|
||||
if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
} else if err != nil {
|
||||
ctx.ServerError("ParseCompareInfo", err)
|
||||
return
|
||||
}
|
||||
|
||||
ci := comparePageInfo.compareInfo
|
||||
ctx.Data["PageIsViewCode"] = true
|
||||
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
|
||||
ctx.Data["CompareInfo"] = ci
|
||||
|
||||
var nothingToCompare bool
|
||||
noMergeBase := ctx.Data["IsNoMergeBase"] == true
|
||||
if noMergeBase {
|
||||
ctx.Flash.Error(ctx.Tr("repo.pulls.no_common_history"), true)
|
||||
ctx.Data["PageIsComparePull"] = false
|
||||
ctx.Data["CommitCount"] = 0
|
||||
nothingToCompare = true
|
||||
} else {
|
||||
nothingToCompare = PrepareCompareDiff(ctx, ci, gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
// TODO: need to refactor "prepare compare" related functions together
|
||||
comparePageInfo.prepareCompareDiff(ctx, gitdiff.GetWhitespaceFlag(GetWhitespaceBehavior(ctx)))
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
baseTags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
|
||||
@@ -682,12 +586,62 @@ func CompareDiff(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if noMergeBase {
|
||||
ctx.HTML(http.StatusOK, tplCompare)
|
||||
if ci.CompareBase != "" {
|
||||
comparePageInfo.prepareCreatePullRequestPage(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
ctx.Flash.Error(ctx.Tr("repo.pulls.no_common_history"), true)
|
||||
ctx.Data["CommitCount"] = 0
|
||||
}
|
||||
ctx.Data["PageIsComparePull"] = comparePageInfo.allowCreatePull
|
||||
ctx.Data["IsNothingToCompare"] = comparePageInfo.nothingToCompare
|
||||
ctx.HTML(http.StatusOK, tplCompare)
|
||||
}
|
||||
|
||||
// DownloadCompareDiff render a comparison's raw unified diff
|
||||
func DownloadCompareDiff(ctx *context.Context) {
|
||||
downloadCompareDiffOrPatch(ctx, false)
|
||||
}
|
||||
|
||||
// DownloadComparePatch render a comparison as a git format-patch
|
||||
func DownloadComparePatch(ctx *context.Context) {
|
||||
downloadCompareDiffOrPatch(ctx, true)
|
||||
}
|
||||
|
||||
func downloadCompareDiffOrPatch(ctx *context.Context, patch bool) {
|
||||
// The route captures `basehead` separately so the `.diff`/`.patch` suffix is
|
||||
// stripped from the catch-all `*` param parseCompareInfo would otherwise read.
|
||||
cpi := newComparePageInfo()
|
||||
if err := cpi.parseCompareInfo(ctx, ctx.PathParam("basehead")); err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.NotFound(nil)
|
||||
} else {
|
||||
ctx.ServerError("ParseCompareInfo", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ci := cpi.compareInfo
|
||||
|
||||
if ctx.Data["PageIsComparePull"] == true {
|
||||
ctx.Resp.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
compareArg := ci.BaseCommitID + ci.CompareSeparator + ci.HeadCommitID
|
||||
|
||||
var err error
|
||||
if patch {
|
||||
err = ci.HeadGitRepo.GetPatch(compareArg, ctx.Resp)
|
||||
} else {
|
||||
err = ci.HeadGitRepo.GetDiff(compareArg, ctx.Resp)
|
||||
}
|
||||
if err != nil {
|
||||
ctx.ServerError("DownloadCompareDiffOrPatch", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (cpi *comparePageInfoType) prepareCreatePullRequestPage(ctx *context.Context) {
|
||||
ci := cpi.compareInfo
|
||||
if cpi.allowCreatePull {
|
||||
pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, ctx.Repo.Repository.ID, ci.HeadRef.ShortName(), ci.BaseRef.ShortName(), issues_model.PullRequestFlowGithub)
|
||||
if err != nil {
|
||||
if !issues_model.IsErrPullRequestNotExist(err) {
|
||||
@@ -701,11 +655,10 @@ func CompareDiff(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
ctx.Data["PullRequest"] = pr
|
||||
ctx.HTML(http.StatusOK, tplCompareDiff)
|
||||
return
|
||||
}
|
||||
|
||||
if !nothingToCompare {
|
||||
if !cpi.nothingToCompare {
|
||||
// Setup information for new form.
|
||||
pageMetaData := retrieveRepoIssueMetaData(ctx, ctx.Repo.Repository, nil, true)
|
||||
if ctx.Written() {
|
||||
@@ -717,8 +670,8 @@ func CompareDiff(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
beforeCommitID := ctx.Data["BeforeCommitID"].(string)
|
||||
afterCommitID := ctx.Data["AfterCommitID"].(string)
|
||||
beforeCommitID := cpi.compareInfo.CompareBase
|
||||
afterCommitID := cpi.compareInfo.HeadCommitID
|
||||
|
||||
ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + ci.CompareSeparator + base.ShortSha(afterCommitID)
|
||||
|
||||
@@ -727,7 +680,7 @@ func CompareDiff(ctx *context.Context) {
|
||||
if content, ok := ctx.Data["content"].(string); ok && content != "" {
|
||||
// If a template content is set, prepend the "content". In this case that's only
|
||||
// applicable if you have one commit to compare and that commit has a message.
|
||||
// In that case the commit message will be prepend to the template body.
|
||||
// In that case the commit message will be prepended to the template body.
|
||||
if templateContent, ok := ctx.Data[pullRequestTemplateKey].(string); ok && templateContent != "" {
|
||||
// Re-use the same key as that's prioritized over the "content" key.
|
||||
// Add two new lines between the content to ensure there's always at least
|
||||
@@ -749,20 +702,14 @@ func CompareDiff(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanWrite(unit.TypeProjects)
|
||||
ctx.Data["IsProjectsEnabled"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
|
||||
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
||||
upload.AddUploadContext(ctx, "comment")
|
||||
|
||||
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypePullRequests)
|
||||
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWrite(unit.TypePullRequests)
|
||||
|
||||
if unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypePullRequests); err == nil {
|
||||
config := unit.PullRequestsConfig()
|
||||
ctx.Data["AllowMaintainerEdit"] = config.DefaultAllowMaintainerEdit
|
||||
} else {
|
||||
ctx.Data["AllowMaintainerEdit"] = false
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplCompare)
|
||||
prConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig()
|
||||
ctx.Data["AllowMaintainerEdit"] = prConfig.DefaultAllowMaintainerEdit
|
||||
}
|
||||
|
||||
// attachCommentsToLines attaches comments to their corresponding diff lines
|
||||
@@ -844,7 +791,7 @@ func ExcerptBlob(ctx *context.Context) {
|
||||
|
||||
diffBlobExcerptData.PullIssueIndex = ctx.FormInt64("pull_issue_index")
|
||||
if diffBlobExcerptData.PullIssueIndex > 0 {
|
||||
if !ctx.Repo.CanRead(unit.TypePullRequests) {
|
||||
if !ctx.Repo.Permission.CanRead(unit.TypePullRequests) {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,16 +6,15 @@ package repo
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
|
||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
git_service "code.gitea.io/gitea/services/git"
|
||||
"code.gitea.io/gitea/services/gitdiff"
|
||||
asymkey_model "gitea.dev/models/asymkey"
|
||||
git_model "gitea.dev/models/git"
|
||||
"gitea.dev/models/gituser"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/setting"
|
||||
git_service "gitea.dev/services/git"
|
||||
"gitea.dev/services/gitdiff"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -53,9 +52,9 @@ func TestNewPullRequestTitleContent(t *testing.T) {
|
||||
mockCommit := func(msg string) *git_model.SignCommitWithStatuses {
|
||||
return &git_model.SignCommitWithStatuses{
|
||||
SignCommit: &asymkey_model.SignCommit{
|
||||
UserCommit: &user_model.UserCommit{
|
||||
Commit: &git.Commit{
|
||||
CommitMessage: msg,
|
||||
UserCommit: &gituser.UserCommit{
|
||||
GitCommit: &git.Commit{
|
||||
CommitMessage: git.CommitMessage{MessageRaw: msg},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -99,9 +98,9 @@ func TestNewPullRequestTitleContent(t *testing.T) {
|
||||
assert.Equal(t, "title-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…", title)
|
||||
assert.Equal(t, "…aaaaaaaaa\n", content)
|
||||
|
||||
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("a\xf0\xf0\xf0\nb\xf0\xf0\xf0")}, setting.RepoPRTitleSourceFirstCommit)
|
||||
assert.Equal(t, "a?", title) // FIXME: GIT-COMMIT-MESSAGE-ENCODING: "title" doesn't use the same charset converting logic as "content"
|
||||
assert.Equal(t, "b"+string(utf8.RuneError)+string(utf8.RuneError), content)
|
||||
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title \xf0\nbody \xf0")}, setting.RepoPRTitleSourceFirstCommit)
|
||||
assert.Equal(t, "title ð", title)
|
||||
assert.Equal(t, "body ð", content)
|
||||
}
|
||||
|
||||
func TestAutoTitleFromBranchName(t *testing.T) {
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
contributors_service "code.gitea.io/gitea/services/repository"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/context"
|
||||
contributors_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -7,16 +7,16 @@ package repo
|
||||
import (
|
||||
"time"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/httpcache"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/lfs"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/routers/common"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
auth_model "gitea.dev/models/auth"
|
||||
git_model "gitea.dev/models/git"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/httpcache"
|
||||
"gitea.dev/modules/httplib"
|
||||
"gitea.dev/modules/lfs"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/storage"
|
||||
"gitea.dev/routers/common"
|
||||
"gitea.dev/services/context"
|
||||
)
|
||||
|
||||
func checkDownloadTokenScope(ctx *context.Context) bool {
|
||||
|
||||
@@ -11,21 +11,21 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
"code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/charset"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/context/upload"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
files_service "code.gitea.io/gitea/services/repository/files"
|
||||
git_model "gitea.dev/models/git"
|
||||
"gitea.dev/models/issues"
|
||||
"gitea.dev/models/unit"
|
||||
"gitea.dev/modules/charset"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/httplib"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/modules/web"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/context/upload"
|
||||
"gitea.dev/services/forms"
|
||||
files_service "gitea.dev/services/repository/files"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/repository/files"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
"gitea.dev/services/repository/files"
|
||||
)
|
||||
|
||||
func NewDiffPatch(ctx *context.Context) {
|
||||
|
||||
@@ -6,14 +6,13 @@ package repo
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/repository/files"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/forms"
|
||||
"gitea.dev/services/repository/files"
|
||||
)
|
||||
|
||||
func CherryPick(ctx *context.Context) {
|
||||
@@ -33,10 +32,10 @@ func CherryPick(ctx *context.Context) {
|
||||
if ctx.FormString("cherry-pick-type") == "revert" {
|
||||
ctx.Data["CherryPickType"] = "revert"
|
||||
ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha")
|
||||
ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message()
|
||||
ctx.Data["commit_message"] = "revert " + cherryPickCommit.MessageUTF8()
|
||||
} else {
|
||||
ctx.Data["CherryPickType"] = "cherry-pick"
|
||||
ctx.Data["commit_summary"], ctx.Data["commit_message"], _ = strings.Cut(cherryPickCommit.Message(), "\n")
|
||||
ctx.Data["commit_summary"], ctx.Data["commit_message"] = cherryPickCommit.MessageTitle(), cherryPickCommit.MessageBody()
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplCherryPick)
|
||||
|
||||
@@ -6,14 +6,14 @@ package repo
|
||||
import (
|
||||
"errors"
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/utils"
|
||||
context_service "code.gitea.io/gitea/services/context"
|
||||
files_service "code.gitea.io/gitea/services/repository/files"
|
||||
git_model "gitea.dev/models/git"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/routers/utils"
|
||||
context_service "gitea.dev/services/context"
|
||||
files_service "gitea.dev/services/repository/files"
|
||||
)
|
||||
|
||||
func errorAs[T error](v error) (e T, ok bool) {
|
||||
@@ -27,13 +27,13 @@ func editorHandleFileOperationErrorRender(ctx *context_service.Context, message,
|
||||
flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{
|
||||
"Message": message,
|
||||
"Summary": summary,
|
||||
"Details": utils.SanitizeFlashErrorString(details),
|
||||
"Details": utils.EscapeFlashErrorString(details),
|
||||
})
|
||||
if err == nil {
|
||||
ctx.JSONError(flashError)
|
||||
} else {
|
||||
log.Error("RenderToHTML: %v", err)
|
||||
ctx.JSONError(message + "\n" + summary + "\n" + utils.SanitizeFlashErrorString(details))
|
||||
log.Error("RenderToHTML(%q, %q, %q), error: %v", message, summary, details, err)
|
||||
ctx.JSONError("Unable to render error details, see server logs") // it should never happen
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ package repo
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
"gitea.dev/modules/templates"
|
||||
"gitea.dev/services/context"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
)
|
||||
|
||||
const tplEditorFork templates.TplName = "repo/editor/fork"
|
||||
|
||||
@@ -6,9 +6,9 @@ package repo
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
files_service "code.gitea.io/gitea/services/repository/files"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/services/context"
|
||||
files_service "gitea.dev/services/repository/files"
|
||||
)
|
||||
|
||||
func DiffPreviewPost(ctx *context.Context) {
|
||||
|
||||
@@ -6,9 +6,9 @@ package repo
|
||||
import (
|
||||
"testing"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -6,12 +6,12 @@ package repo
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/context/upload"
|
||||
files_service "code.gitea.io/gitea/services/repository/files"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/services/context"
|
||||
"gitea.dev/services/context/upload"
|
||||
files_service "gitea.dev/services/repository/files"
|
||||
)
|
||||
|
||||
// UploadFileToServer upload file to server file dir not git
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user