mirror of
https://github.com/go-gitea/gitea.git
synced 2025-01-02 01:17:43 +03:00
Compare commits
34 Commits
9cdf3b723d
...
f1e120096c
Author | SHA1 | Date | |
---|---|---|---|
|
f1e120096c | ||
|
a96776b3cb | ||
|
ff96873e3e | ||
|
0ed160ffea | ||
|
e95b946f6d | ||
|
64bebc9402 | ||
|
94048f3035 | ||
|
a92f5057ae | ||
|
3d3ece36d2 | ||
|
e69da2cd07 | ||
|
e435b1900a | ||
|
254314be5f | ||
|
14ed553fae | ||
|
079a1ffe8f | ||
|
ea198f9ea8 | ||
|
a7b2707be9 | ||
|
2d1a171dc7 | ||
|
3c00e89129 | ||
|
df98452c0d | ||
|
44b4fb21a4 | ||
|
7bb7ba1b5b | ||
|
ae44ed2cf6 | ||
|
8444c78397 | ||
|
b91d59715a | ||
|
cadfbad2b5 | ||
|
f9c54b8bf7 | ||
|
25f20624ab | ||
|
1b63310bd1 | ||
|
8d365c2573 | ||
|
f0d3dcaf60 | ||
|
21f7357085 | ||
|
dee562ea9a | ||
|
fd4ad21ee2 | ||
|
7831654e38 |
@ -674,7 +674,7 @@ module.exports = {
|
||||
'no-this-before-super': [2],
|
||||
'no-throw-literal': [2],
|
||||
'no-undef-init': [2],
|
||||
'no-undef': [0],
|
||||
'no-undef': [2], // it is still needed by eslint & IDE to prompt undefined names in real time
|
||||
'no-undefined': [0],
|
||||
'no-underscore-dangle': [0],
|
||||
'no-unexpected-multiline': [2],
|
||||
|
@ -1339,6 +1339,9 @@ LEVEL = Info
|
||||
;; Number of repos that are displayed on one page
|
||||
;REPO_PAGING_NUM = 15
|
||||
|
||||
;; Number of orgs that are displayed on profile page
|
||||
;ORG_PAGING_NUM = 15
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;[ui.meta]
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
@ -337,8 +338,10 @@ func newlyCreatedIssues(ctx context.Context, repoID int64, fromTime time.Time) *
|
||||
func activeIssues(ctx context.Context, repoID int64, fromTime time.Time) *xorm.Session {
|
||||
sess := db.GetEngine(ctx).Where("issue.repo_id = ?", repoID).
|
||||
And("issue.is_pull = ?", false).
|
||||
And("issue.created_unix >= ?", fromTime.Unix()).
|
||||
Or("issue.closed_unix >= ?", fromTime.Unix())
|
||||
And(builder.Or(
|
||||
builder.Gte{"issue.created_unix": fromTime.Unix()},
|
||||
builder.Gte{"issue.closed_unix": fromTime.Unix()},
|
||||
))
|
||||
|
||||
return sess
|
||||
}
|
||||
|
@ -64,11 +64,9 @@ func TestGetUserHeatmapDataByUser(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.userID})
|
||||
|
||||
doer := &user_model.User{ID: tc.doerID}
|
||||
_, err := unittest.LoadBeanIfExists(doer)
|
||||
assert.NoError(t, err)
|
||||
if tc.doerID == 0 {
|
||||
doer = nil
|
||||
var doer *user_model.User
|
||||
if tc.doerID != 0 {
|
||||
doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: tc.doerID})
|
||||
}
|
||||
|
||||
// get the action for comparison
|
||||
|
@ -44,7 +44,7 @@ func TestWebAuthnCredential_UpdateSignCount(t *testing.T) {
|
||||
cred := unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1})
|
||||
cred.SignCount = 1
|
||||
assert.NoError(t, cred.UpdateSignCount(db.DefaultContext))
|
||||
unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{ID: 1, SignCount: 1})
|
||||
unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1, SignCount: 1})
|
||||
}
|
||||
|
||||
func TestWebAuthnCredential_UpdateLargeCounter(t *testing.T) {
|
||||
@ -52,7 +52,7 @@ func TestWebAuthnCredential_UpdateLargeCounter(t *testing.T) {
|
||||
cred := unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1})
|
||||
cred.SignCount = 0xffffffff
|
||||
assert.NoError(t, cred.UpdateSignCount(db.DefaultContext))
|
||||
unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{ID: 1, SignCount: 0xffffffff})
|
||||
unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1, SignCount: 0xffffffff})
|
||||
}
|
||||
|
||||
func TestCreateCredential(t *testing.T) {
|
||||
@ -63,5 +63,5 @@ func TestCreateCredential(t *testing.T) {
|
||||
assert.Equal(t, "WebAuthn Created Credential", res.Name)
|
||||
assert.Equal(t, []byte("Test"), res.CredentialID)
|
||||
|
||||
unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1})
|
||||
unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1})
|
||||
}
|
||||
|
@ -96,3 +96,14 @@
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
archived_unix: 0
|
||||
|
||||
-
|
||||
id: 10
|
||||
repo_id: 3
|
||||
org_id: 0
|
||||
name: repo3label1
|
||||
color: '#112233'
|
||||
exclusive: false
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
archived_unix: 0
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_NewIssueUsers(t *testing.T) {
|
||||
@ -27,9 +28,8 @@ func Test_NewIssueUsers(t *testing.T) {
|
||||
}
|
||||
|
||||
// artificially insert new issue
|
||||
unittest.AssertSuccessfulInsert(t, newIssue)
|
||||
|
||||
assert.NoError(t, issues_model.NewIssueUsers(db.DefaultContext, repo, newIssue))
|
||||
require.NoError(t, db.Insert(db.DefaultContext, newIssue))
|
||||
require.NoError(t, issues_model.NewIssueUsers(db.DefaultContext, repo, newIssue))
|
||||
|
||||
// issue_user table should now have entries for new issue
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: newIssue.ID, UID: newIssue.PosterID})
|
||||
|
@ -349,6 +349,17 @@ func GetLabelIDsInRepoByNames(ctx context.Context, repoID int64, labelNames []st
|
||||
Find(&labelIDs)
|
||||
}
|
||||
|
||||
// GetLabelIDsInOrgByNames returns a list of labelIDs by names in a given org.
|
||||
func GetLabelIDsInOrgByNames(ctx context.Context, orgID int64, labelNames []string) ([]int64, error) {
|
||||
labelIDs := make([]int64, 0, len(labelNames))
|
||||
return labelIDs, db.GetEngine(ctx).Table("label").
|
||||
Where("org_id = ?", orgID).
|
||||
In("name", labelNames).
|
||||
Asc("name").
|
||||
Cols("id").
|
||||
Find(&labelIDs)
|
||||
}
|
||||
|
||||
// BuildLabelNamesIssueIDsCondition returns a builder where get issue ids match label names
|
||||
func BuildLabelNamesIssueIDsCondition(labelNames []string) *builder.Builder {
|
||||
return builder.Select("issue_label.issue_id").
|
||||
|
@ -387,7 +387,7 @@ func TestDeleteIssueLabel(t *testing.T) {
|
||||
|
||||
expectedNumIssues := label.NumIssues
|
||||
expectedNumClosedIssues := label.NumClosedIssues
|
||||
if unittest.BeanExists(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) {
|
||||
if unittest.GetBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) != nil {
|
||||
expectedNumIssues--
|
||||
if issue.IsClosed {
|
||||
expectedNumClosedIssues--
|
||||
|
@ -131,7 +131,7 @@ func TestAddOrgUser(t *testing.T) {
|
||||
testSuccess := func(orgID, userID int64, isPublic bool) {
|
||||
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: orgID})
|
||||
expectedNumMembers := org.NumMembers
|
||||
if !unittest.BeanExists(t, &organization.OrgUser{OrgID: orgID, UID: userID}) {
|
||||
if unittest.GetBean(t, &organization.OrgUser{OrgID: orgID, UID: userID}) == nil {
|
||||
expectedNumMembers++
|
||||
}
|
||||
assert.NoError(t, organization.AddOrgUser(db.DefaultContext, orgID, userID))
|
||||
|
@ -1,12 +1,10 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//nolint:forbidigo
|
||||
package unittest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
@ -37,7 +35,7 @@ func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
|
||||
} else {
|
||||
fixtureOptionFiles = testfixtures.Files(opts.Files...)
|
||||
}
|
||||
dialect := "unknown"
|
||||
var dialect string
|
||||
switch e.Dialect().URI().DBType {
|
||||
case schemas.POSTGRES:
|
||||
dialect = "postgres"
|
||||
@ -48,8 +46,7 @@ func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
|
||||
case schemas.SQLITE:
|
||||
dialect = "sqlite3"
|
||||
default:
|
||||
fmt.Println("Unsupported RDBMS for integration tests")
|
||||
os.Exit(1)
|
||||
return fmt.Errorf("unsupported RDBMS for integration tests: %q", e.Dialect().URI().DBType)
|
||||
}
|
||||
loaderOptions := []func(loader *testfixtures.Loader) error{
|
||||
testfixtures.Database(e.DB().DB),
|
||||
@ -69,9 +66,7 @@ func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
|
||||
|
||||
// register the dummy hash algorithm function used in the test fixtures
|
||||
_ = hash.Register("dummy", hash.NewDummyHasher)
|
||||
|
||||
setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@ -87,7 +82,7 @@ func LoadFixtures(engine ...*xorm.Engine) error {
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Printf("LoadFixtures failed after retries: %v\n", err)
|
||||
return fmt.Errorf("LoadFixtures failed after retries: %w", err)
|
||||
}
|
||||
// Now if we're running postgres we need to tell it to update the sequences
|
||||
if e.Dialect().URI().DBType == schemas.POSTGRES {
|
||||
@ -108,21 +103,18 @@ func LoadFixtures(engine ...*xorm.Engine) error {
|
||||
AND T.relname = PGT.tablename
|
||||
ORDER BY S.relname;`)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to generate sequence update: %v\n", err)
|
||||
return err
|
||||
return fmt.Errorf("failed to generate sequence update: %w", err)
|
||||
}
|
||||
for _, r := range results {
|
||||
for _, value := range r {
|
||||
_, err = e.Exec(value)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to update sequence: %s Error: %v\n", value, err)
|
||||
return err
|
||||
return fmt.Errorf("failed to update sequence: %s, error: %w", value, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = hash.Register("dummy", hash.NewDummyHasher)
|
||||
setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy")
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
package unittest
|
||||
|
||||
import (
|
||||
"log"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
@ -14,7 +14,7 @@ func fieldByName(v reflect.Value, field string) reflect.Value {
|
||||
}
|
||||
f := v.FieldByName(field)
|
||||
if !f.IsValid() {
|
||||
log.Panicf("can not read %s for %v", field, v)
|
||||
panic(fmt.Errorf("can not read %s for %v", field, v))
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
@ -34,11 +34,6 @@ var (
|
||||
fixturesDir string
|
||||
)
|
||||
|
||||
// FixturesDir returns the fixture directory
|
||||
func FixturesDir() string {
|
||||
return fixturesDir
|
||||
}
|
||||
|
||||
func fatalTestError(fmtStr string, args ...any) {
|
||||
_, _ = fmt.Fprintf(os.Stderr, fmtStr, args...)
|
||||
os.Exit(1)
|
||||
|
@ -4,13 +4,17 @@
|
||||
package unittest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// Code in this file is mainly used by unittest.CheckConsistencyFor, which is not in the unit test for various reasons.
|
||||
@ -51,22 +55,23 @@ func whereOrderConditions(e db.Engine, conditions []any) db.Engine {
|
||||
return e.OrderBy(orderBy)
|
||||
}
|
||||
|
||||
// LoadBeanIfExists loads beans from fixture database if exist
|
||||
func LoadBeanIfExists(bean any, conditions ...any) (bool, error) {
|
||||
func getBeanIfExists(bean any, conditions ...any) (bool, error) {
|
||||
e := db.GetEngine(db.DefaultContext)
|
||||
return whereOrderConditions(e, conditions).Get(bean)
|
||||
}
|
||||
|
||||
// BeanExists for testing, check if a bean exists
|
||||
func BeanExists(t assert.TestingT, bean any, conditions ...any) bool {
|
||||
exists, err := LoadBeanIfExists(bean, conditions...)
|
||||
assert.NoError(t, err)
|
||||
return exists
|
||||
func GetBean[T any](t require.TestingT, bean T, conditions ...any) (ret T) {
|
||||
exists, err := getBeanIfExists(bean, conditions...)
|
||||
require.NoError(t, err)
|
||||
if exists {
|
||||
return bean
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// AssertExistsAndLoadBean assert that a bean exists and load it from the test database
|
||||
func AssertExistsAndLoadBean[T any](t require.TestingT, bean T, conditions ...any) T {
|
||||
exists, err := LoadBeanIfExists(bean, conditions...)
|
||||
exists, err := getBeanIfExists(bean, conditions...)
|
||||
require.NoError(t, err)
|
||||
require.True(t, exists,
|
||||
"Expected to find %+v (of type %T, with conditions %+v), but did not",
|
||||
@ -112,25 +117,11 @@ func GetCount(t assert.TestingT, bean any, conditions ...any) int {
|
||||
|
||||
// AssertNotExistsBean assert that a bean does not exist in the test database
|
||||
func AssertNotExistsBean(t assert.TestingT, bean any, conditions ...any) {
|
||||
exists, err := LoadBeanIfExists(bean, conditions...)
|
||||
exists, err := getBeanIfExists(bean, conditions...)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, exists)
|
||||
}
|
||||
|
||||
// AssertExistsIf asserts that a bean exists or does not exist, depending on
|
||||
// what is expected.
|
||||
func AssertExistsIf(t assert.TestingT, expected bool, bean any, conditions ...any) {
|
||||
exists, err := LoadBeanIfExists(bean, conditions...)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, exists)
|
||||
}
|
||||
|
||||
// AssertSuccessfulInsert assert that beans is successfully inserted
|
||||
func AssertSuccessfulInsert(t assert.TestingT, beans ...any) {
|
||||
err := db.Insert(db.DefaultContext, beans...)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// AssertCount assert the count of a bean
|
||||
func AssertCount(t assert.TestingT, bean, expected any) bool {
|
||||
return assert.EqualValues(t, expected, GetCount(t, bean))
|
||||
@ -155,3 +146,39 @@ func AssertCountByCond(t assert.TestingT, tableName string, cond builder.Cond, e
|
||||
return assert.EqualValues(t, expected, GetCountByCond(t, tableName, cond),
|
||||
"Failed consistency test, the counted bean (of table %s) was %+v", tableName, cond)
|
||||
}
|
||||
|
||||
// DumpQueryResult dumps the result of a query for debugging purpose
|
||||
func DumpQueryResult(t require.TestingT, sqlOrBean any, sqlArgs ...any) {
|
||||
x := db.GetEngine(db.DefaultContext).(*xorm.Engine)
|
||||
goDB := x.DB().DB
|
||||
sql, ok := sqlOrBean.(string)
|
||||
if !ok {
|
||||
sql = fmt.Sprintf("SELECT * FROM %s", db.TableName(sqlOrBean))
|
||||
} else if !strings.Contains(sql, " ") {
|
||||
sql = fmt.Sprintf("SELECT * FROM %s", sql)
|
||||
}
|
||||
rows, err := goDB.Query(sql, sqlArgs...)
|
||||
require.NoError(t, err)
|
||||
defer rows.Close()
|
||||
columns, err := rows.Columns()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _ = fmt.Fprintf(os.Stdout, "====== DumpQueryResult: %s ======\n", sql)
|
||||
idx := 0
|
||||
for rows.Next() {
|
||||
row := make([]any, len(columns))
|
||||
rowPointers := make([]any, len(columns))
|
||||
for i := range row {
|
||||
rowPointers[i] = &row[i]
|
||||
}
|
||||
require.NoError(t, rows.Scan(rowPointers...))
|
||||
_, _ = fmt.Fprintf(os.Stdout, "- # row[%d]\n", idx)
|
||||
for i, col := range columns {
|
||||
_, _ = fmt.Fprintf(os.Stdout, " %s: %v\n", col, row[i])
|
||||
}
|
||||
idx++
|
||||
}
|
||||
if idx == 0 {
|
||||
_, _ = fmt.Fprintf(os.Stdout, "(no result, columns: %s)\n", strings.Join(columns, ", "))
|
||||
}
|
||||
}
|
||||
|
@ -357,8 +357,8 @@ func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddres
|
||||
if user := GetVerifyUser(ctx, code); user != nil {
|
||||
// time limit code
|
||||
prefix := code[:base.TimeLimitCodeLength]
|
||||
data := fmt.Sprintf("%d%s%s%s%s", user.ID, email, user.LowerName, user.Passwd, user.Rands)
|
||||
|
||||
opts := &TimeLimitCodeOptions{Purpose: TimeLimitCodeActivateEmail, NewEmail: email}
|
||||
data := makeTimeLimitCodeHashData(opts, user)
|
||||
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
|
||||
emailAddress := &EmailAddress{UID: user.ID, Email: email}
|
||||
if has, _ := db.GetEngine(ctx).Get(emailAddress); has {
|
||||
@ -486,10 +486,10 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate
|
||||
|
||||
// Activate/deactivate a user's primary email address and account
|
||||
if addr.IsPrimary {
|
||||
user, exist, err := db.Get[User](ctx, builder.Eq{"id": userID, "email": email})
|
||||
user, exist, err := db.Get[User](ctx, builder.Eq{"id": userID})
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !exist {
|
||||
} else if !exist || !strings.EqualFold(user.Email, email) {
|
||||
return fmt.Errorf("no user with ID: %d and Email: %s", userID, email)
|
||||
}
|
||||
|
||||
|
@ -181,7 +181,8 @@ func (u *User) BeforeUpdate() {
|
||||
u.MaxRepoCreation = -1
|
||||
}
|
||||
|
||||
// Organization does not need email
|
||||
// FIXME: this email doesn't need to be in lowercase, because the emails are mainly managed by the email table with lower_email field
|
||||
// This trick could be removed in new releases to display the user inputed email as-is.
|
||||
u.Email = strings.ToLower(u.Email)
|
||||
if !u.IsOrganization() {
|
||||
if len(u.AvatarEmail) == 0 {
|
||||
@ -310,17 +311,6 @@ func (u *User) OrganisationLink() string {
|
||||
return setting.AppSubURL + "/org/" + url.PathEscape(u.Name)
|
||||
}
|
||||
|
||||
// GenerateEmailActivateCode generates an activate code based on user information and given e-mail.
|
||||
func (u *User) GenerateEmailActivateCode(email string) string {
|
||||
code := base.CreateTimeLimitCode(
|
||||
fmt.Sprintf("%d%s%s%s%s", u.ID, email, u.LowerName, u.Passwd, u.Rands),
|
||||
setting.Service.ActiveCodeLives, time.Now(), nil)
|
||||
|
||||
// Add tail hex username
|
||||
code += hex.EncodeToString([]byte(u.LowerName))
|
||||
return code
|
||||
}
|
||||
|
||||
// GetUserFollowers returns range of user's followers.
|
||||
func GetUserFollowers(ctx context.Context, u, viewer *User, listOptions db.ListOptions) ([]*User, int64, error) {
|
||||
sess := db.GetEngine(ctx).
|
||||
@ -863,12 +853,38 @@ func GetVerifyUser(ctx context.Context, code string) (user *User) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyUserActiveCode verifies active code when active account
|
||||
func VerifyUserActiveCode(ctx context.Context, code string) (user *User) {
|
||||
type TimeLimitCodePurpose string
|
||||
|
||||
const (
|
||||
TimeLimitCodeActivateAccount TimeLimitCodePurpose = "activate_account"
|
||||
TimeLimitCodeActivateEmail TimeLimitCodePurpose = "activate_email"
|
||||
TimeLimitCodeResetPassword TimeLimitCodePurpose = "reset_password"
|
||||
)
|
||||
|
||||
type TimeLimitCodeOptions struct {
|
||||
Purpose TimeLimitCodePurpose
|
||||
NewEmail string
|
||||
}
|
||||
|
||||
func makeTimeLimitCodeHashData(opts *TimeLimitCodeOptions, u *User) string {
|
||||
return fmt.Sprintf("%s|%d|%s|%s|%s|%s", opts.Purpose, u.ID, strings.ToLower(util.IfZero(opts.NewEmail, u.Email)), u.LowerName, u.Passwd, u.Rands)
|
||||
}
|
||||
|
||||
// GenerateUserTimeLimitCode generates a time-limit code based on user information and given e-mail.
|
||||
// TODO: need to use cache or db to store it to make sure a code can only be consumed once
|
||||
func GenerateUserTimeLimitCode(opts *TimeLimitCodeOptions, u *User) string {
|
||||
data := makeTimeLimitCodeHashData(opts, u)
|
||||
code := base.CreateTimeLimitCode(data, setting.Service.ActiveCodeLives, time.Now(), nil)
|
||||
code += hex.EncodeToString([]byte(u.LowerName)) // Add tail hex username
|
||||
return code
|
||||
}
|
||||
|
||||
// VerifyUserTimeLimitCode verifies the time-limit code
|
||||
func VerifyUserTimeLimitCode(ctx context.Context, opts *TimeLimitCodeOptions, code string) (user *User) {
|
||||
if user = GetVerifyUser(ctx, code); user != nil {
|
||||
// time limit code
|
||||
prefix := code[:base.TimeLimitCodeLength]
|
||||
data := fmt.Sprintf("%d%s%s%s%s", user.ID, user.Email, user.LowerName, user.Passwd, user.Rands)
|
||||
data := makeTimeLimitCodeHashData(opts, user)
|
||||
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
|
||||
return user
|
||||
}
|
||||
|
@ -63,6 +63,7 @@ var UI = struct {
|
||||
} `ini:"ui.admin"`
|
||||
User struct {
|
||||
RepoPagingNum int
|
||||
OrgPagingNum int
|
||||
} `ini:"ui.user"`
|
||||
Meta struct {
|
||||
Author string
|
||||
@ -127,8 +128,10 @@ var UI = struct {
|
||||
},
|
||||
User: struct {
|
||||
RepoPagingNum int
|
||||
OrgPagingNum int
|
||||
}{
|
||||
RepoPagingNum: 15,
|
||||
OrgPagingNum: 15,
|
||||
},
|
||||
Meta: struct {
|
||||
Author string
|
||||
|
@ -4,18 +4,14 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/reqctx"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
|
||||
"gitea.com/go-chi/binding"
|
||||
@ -45,7 +41,7 @@ func GetForm(dataStore reqctx.RequestDataStore) any {
|
||||
|
||||
// Router defines a route based on chi's router
|
||||
type Router struct {
|
||||
chiRouter chi.Router
|
||||
chiRouter *chi.Mux
|
||||
curGroupPrefix string
|
||||
curMiddlewares []any
|
||||
}
|
||||
@ -97,16 +93,21 @@ func isNilOrFuncNil(v any) bool {
|
||||
return r.Kind() == reflect.Func && r.IsNil()
|
||||
}
|
||||
|
||||
func (r *Router) wrapMiddlewareAndHandler(h []any) ([]func(http.Handler) http.Handler, http.HandlerFunc) {
|
||||
handlerProviders := make([]func(http.Handler) http.Handler, 0, len(r.curMiddlewares)+len(h)+1)
|
||||
for _, m := range r.curMiddlewares {
|
||||
func wrapMiddlewareAndHandler(curMiddlewares, h []any) ([]func(http.Handler) http.Handler, http.HandlerFunc) {
|
||||
handlerProviders := make([]func(http.Handler) http.Handler, 0, len(curMiddlewares)+len(h)+1)
|
||||
for _, m := range curMiddlewares {
|
||||
if !isNilOrFuncNil(m) {
|
||||
handlerProviders = append(handlerProviders, toHandlerProvider(m))
|
||||
}
|
||||
}
|
||||
for _, m := range h {
|
||||
if len(h) == 0 {
|
||||
panic("no endpoint handler provided")
|
||||
}
|
||||
for i, m := range h {
|
||||
if !isNilOrFuncNil(m) {
|
||||
handlerProviders = append(handlerProviders, toHandlerProvider(m))
|
||||
} else if i == len(h)-1 {
|
||||
panic("endpoint handler can't be nil")
|
||||
}
|
||||
}
|
||||
middlewares := handlerProviders[:len(handlerProviders)-1]
|
||||
@ -121,7 +122,7 @@ func (r *Router) wrapMiddlewareAndHandler(h []any) ([]func(http.Handler) http.Ha
|
||||
// Methods adds the same handlers for multiple http "methods" (separated by ",").
|
||||
// If any method is invalid, the lower level router will panic.
|
||||
func (r *Router) Methods(methods, pattern string, h ...any) {
|
||||
middlewares, handlerFunc := r.wrapMiddlewareAndHandler(h)
|
||||
middlewares, handlerFunc := wrapMiddlewareAndHandler(r.curMiddlewares, h)
|
||||
fullPattern := r.getPattern(pattern)
|
||||
if strings.Contains(methods, ",") {
|
||||
methods := strings.Split(methods, ",")
|
||||
@ -141,7 +142,7 @@ func (r *Router) Mount(pattern string, subRouter *Router) {
|
||||
|
||||
// Any delegate requests for all methods
|
||||
func (r *Router) Any(pattern string, h ...any) {
|
||||
middlewares, handlerFunc := r.wrapMiddlewareAndHandler(h)
|
||||
middlewares, handlerFunc := wrapMiddlewareAndHandler(r.curMiddlewares, h)
|
||||
r.chiRouter.With(middlewares...).HandleFunc(r.getPattern(pattern), handlerFunc)
|
||||
}
|
||||
|
||||
@ -185,17 +186,6 @@ func (r *Router) NotFound(h http.HandlerFunc) {
|
||||
r.chiRouter.NotFound(h)
|
||||
}
|
||||
|
||||
type pathProcessorParam struct {
|
||||
name string
|
||||
captureGroup int
|
||||
}
|
||||
|
||||
type PathProcessor struct {
|
||||
methods container.Set[string]
|
||||
re *regexp.Regexp
|
||||
params []pathProcessorParam
|
||||
}
|
||||
|
||||
func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Request, next http.Handler) {
|
||||
normalized := false
|
||||
normalizedPath := req.URL.EscapedPath()
|
||||
@ -253,121 +243,16 @@ func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Reques
|
||||
next.ServeHTTP(resp, req)
|
||||
}
|
||||
|
||||
func (p *PathProcessor) ProcessRequestPath(chiCtx *chi.Context, path string) bool {
|
||||
if !p.methods.Contains(chiCtx.RouteMethod) {
|
||||
return false
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
pathMatches := p.re.FindStringSubmatchIndex(path) // Golang regexp match pairs [start, end, start, end, ...]
|
||||
if pathMatches == nil {
|
||||
return false
|
||||
}
|
||||
var paramMatches [][]int
|
||||
for i := 2; i < len(pathMatches); {
|
||||
paramMatches = append(paramMatches, []int{pathMatches[i], pathMatches[i+1]})
|
||||
pmIdx := len(paramMatches) - 1
|
||||
end := pathMatches[i+1]
|
||||
i += 2
|
||||
for ; i < len(pathMatches); i += 2 {
|
||||
if pathMatches[i] >= end {
|
||||
break
|
||||
}
|
||||
paramMatches[pmIdx] = append(paramMatches[pmIdx], pathMatches[i], pathMatches[i+1])
|
||||
}
|
||||
}
|
||||
for i, pm := range paramMatches {
|
||||
groupIdx := p.params[i].captureGroup * 2
|
||||
chiCtx.URLParams.Add(p.params[i].name, path[pm[groupIdx]:pm[groupIdx+1]])
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func NewPathProcessor(methods, pattern string) *PathProcessor {
|
||||
p := &PathProcessor{methods: make(container.Set[string])}
|
||||
for _, method := range strings.Split(methods, ",") {
|
||||
p.methods.Add(strings.TrimSpace(method))
|
||||
}
|
||||
re := []byte{'^'}
|
||||
lastEnd := 0
|
||||
for lastEnd < len(pattern) {
|
||||
start := strings.IndexByte(pattern[lastEnd:], '<')
|
||||
if start == -1 {
|
||||
re = append(re, pattern[lastEnd:]...)
|
||||
break
|
||||
}
|
||||
end := strings.IndexByte(pattern[lastEnd+start:], '>')
|
||||
if end == -1 {
|
||||
panic(fmt.Sprintf("invalid pattern: %s", pattern))
|
||||
}
|
||||
re = append(re, pattern[lastEnd:lastEnd+start]...)
|
||||
partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":")
|
||||
lastEnd += start + end + 1
|
||||
|
||||
// TODO: it could support to specify a "capture group" for the name, for example: "/<name[2]:(\d)-(\d)>"
|
||||
// it is not used so no need to implement it now
|
||||
param := pathProcessorParam{}
|
||||
if partExp == "*" {
|
||||
re = append(re, "(.*?)/?"...)
|
||||
if lastEnd < len(pattern) {
|
||||
if pattern[lastEnd] == '/' {
|
||||
lastEnd++
|
||||
}
|
||||
}
|
||||
} else {
|
||||
partExp = util.IfZero(partExp, "[^/]+")
|
||||
re = append(re, '(')
|
||||
re = append(re, partExp...)
|
||||
re = append(re, ')')
|
||||
}
|
||||
param.name = partName
|
||||
p.params = append(p.params, param)
|
||||
}
|
||||
re = append(re, '$')
|
||||
reStr := string(re)
|
||||
p.re = regexp.MustCompile(reStr)
|
||||
return p
|
||||
}
|
||||
|
||||
// Combo delegates requests to Combo
|
||||
func (r *Router) Combo(pattern string, h ...any) *Combo {
|
||||
return &Combo{r, pattern, h}
|
||||
}
|
||||
|
||||
// Combo represents a tiny group routes with same pattern
|
||||
type Combo struct {
|
||||
r *Router
|
||||
pattern string
|
||||
h []any
|
||||
}
|
||||
|
||||
// Get delegates Get method
|
||||
func (c *Combo) Get(h ...any) *Combo {
|
||||
c.r.Get(c.pattern, append(c.h, h...)...)
|
||||
return c
|
||||
}
|
||||
|
||||
// Post delegates Post method
|
||||
func (c *Combo) Post(h ...any) *Combo {
|
||||
c.r.Post(c.pattern, append(c.h, h...)...)
|
||||
return c
|
||||
}
|
||||
|
||||
// Delete delegates Delete method
|
||||
func (c *Combo) Delete(h ...any) *Combo {
|
||||
c.r.Delete(c.pattern, append(c.h, h...)...)
|
||||
return c
|
||||
}
|
||||
|
||||
// Put delegates Put method
|
||||
func (c *Combo) Put(h ...any) *Combo {
|
||||
c.r.Put(c.pattern, append(c.h, h...)...)
|
||||
return c
|
||||
}
|
||||
|
||||
// Patch delegates Patch method
|
||||
func (c *Combo) Patch(h ...any) *Combo {
|
||||
c.r.Patch(c.pattern, append(c.h, h...)...)
|
||||
return c
|
||||
// PathGroup creates a group of paths which could be matched by regexp.
|
||||
// It is only designed to resolve some special cases which chi router can't handle.
|
||||
// For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient).
|
||||
func (r *Router) PathGroup(pattern string, fn func(g *RouterPathGroup), h ...any) {
|
||||
g := &RouterPathGroup{r: r, pathParam: "*"}
|
||||
fn(g)
|
||||
r.Any(pattern, append(h, g.ServeHTTP)...)
|
||||
}
|
41
modules/web/router_combo.go
Normal file
41
modules/web/router_combo.go
Normal file
@ -0,0 +1,41 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package web
|
||||
|
||||
// Combo represents a tiny group routes with same pattern
|
||||
type Combo struct {
|
||||
r *Router
|
||||
pattern string
|
||||
h []any
|
||||
}
|
||||
|
||||
// Get delegates Get method
|
||||
func (c *Combo) Get(h ...any) *Combo {
|
||||
c.r.Get(c.pattern, append(c.h, h...)...)
|
||||
return c
|
||||
}
|
||||
|
||||
// Post delegates Post method
|
||||
func (c *Combo) Post(h ...any) *Combo {
|
||||
c.r.Post(c.pattern, append(c.h, h...)...)
|
||||
return c
|
||||
}
|
||||
|
||||
// Delete delegates Delete method
|
||||
func (c *Combo) Delete(h ...any) *Combo {
|
||||
c.r.Delete(c.pattern, append(c.h, h...)...)
|
||||
return c
|
||||
}
|
||||
|
||||
// Put delegates Put method
|
||||
func (c *Combo) Put(h ...any) *Combo {
|
||||
c.r.Put(c.pattern, append(c.h, h...)...)
|
||||
return c
|
||||
}
|
||||
|
||||
// Patch delegates Patch method
|
||||
func (c *Combo) Patch(h ...any) *Combo {
|
||||
c.r.Patch(c.pattern, append(c.h, h...)...)
|
||||
return c
|
||||
}
|
135
modules/web/router_path.go
Normal file
135
modules/web/router_path.go
Normal file
@ -0,0 +1,135 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type RouterPathGroup struct {
|
||||
r *Router
|
||||
pathParam string
|
||||
matchers []*routerPathMatcher
|
||||
}
|
||||
|
||||
func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||
chiCtx := chi.RouteContext(req.Context())
|
||||
path := chiCtx.URLParam(g.pathParam)
|
||||
for _, m := range g.matchers {
|
||||
if m.matchPath(chiCtx, path) {
|
||||
handler := m.handlerFunc
|
||||
for i := len(m.middlewares) - 1; i >= 0; i-- {
|
||||
handler = m.middlewares[i](handler).ServeHTTP
|
||||
}
|
||||
handler(resp, req)
|
||||
return
|
||||
}
|
||||
}
|
||||
g.r.chiRouter.NotFoundHandler().ServeHTTP(resp, req)
|
||||
}
|
||||
|
||||
// MatchPath matches the request method, and uses regexp to match the path.
|
||||
// The pattern uses "<...>" to define path parameters, for example: "/<name>" (different from chi router)
|
||||
// It is only designed to resolve some special cases which chi router can't handle.
|
||||
// For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient).
|
||||
func (g *RouterPathGroup) MatchPath(methods, pattern string, h ...any) {
|
||||
g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...))
|
||||
}
|
||||
|
||||
type routerPathParam struct {
|
||||
name string
|
||||
captureGroup int
|
||||
}
|
||||
|
||||
type routerPathMatcher struct {
|
||||
methods container.Set[string]
|
||||
re *regexp.Regexp
|
||||
params []routerPathParam
|
||||
middlewares []func(http.Handler) http.Handler
|
||||
handlerFunc http.HandlerFunc
|
||||
}
|
||||
|
||||
func (p *routerPathMatcher) matchPath(chiCtx *chi.Context, path string) bool {
|
||||
if !p.methods.Contains(chiCtx.RouteMethod) {
|
||||
return false
|
||||
}
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
pathMatches := p.re.FindStringSubmatchIndex(path) // Golang regexp match pairs [start, end, start, end, ...]
|
||||
if pathMatches == nil {
|
||||
return false
|
||||
}
|
||||
var paramMatches [][]int
|
||||
for i := 2; i < len(pathMatches); {
|
||||
paramMatches = append(paramMatches, []int{pathMatches[i], pathMatches[i+1]})
|
||||
pmIdx := len(paramMatches) - 1
|
||||
end := pathMatches[i+1]
|
||||
i += 2
|
||||
for ; i < len(pathMatches); i += 2 {
|
||||
if pathMatches[i] >= end {
|
||||
break
|
||||
}
|
||||
paramMatches[pmIdx] = append(paramMatches[pmIdx], pathMatches[i], pathMatches[i+1])
|
||||
}
|
||||
}
|
||||
for i, pm := range paramMatches {
|
||||
groupIdx := p.params[i].captureGroup * 2
|
||||
chiCtx.URLParams.Add(p.params[i].name, path[pm[groupIdx]:pm[groupIdx+1]])
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher {
|
||||
middlewares, handlerFunc := wrapMiddlewareAndHandler(nil, h)
|
||||
p := &routerPathMatcher{methods: make(container.Set[string]), middlewares: middlewares, handlerFunc: handlerFunc}
|
||||
for _, method := range strings.Split(methods, ",") {
|
||||
p.methods.Add(strings.TrimSpace(method))
|
||||
}
|
||||
re := []byte{'^'}
|
||||
lastEnd := 0
|
||||
for lastEnd < len(pattern) {
|
||||
start := strings.IndexByte(pattern[lastEnd:], '<')
|
||||
if start == -1 {
|
||||
re = append(re, pattern[lastEnd:]...)
|
||||
break
|
||||
}
|
||||
end := strings.IndexByte(pattern[lastEnd+start:], '>')
|
||||
if end == -1 {
|
||||
panic(fmt.Sprintf("invalid pattern: %s", pattern))
|
||||
}
|
||||
re = append(re, pattern[lastEnd:lastEnd+start]...)
|
||||
partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":")
|
||||
lastEnd += start + end + 1
|
||||
|
||||
// TODO: it could support to specify a "capture group" for the name, for example: "/<name[2]:(\d)-(\d)>"
|
||||
// it is not used so no need to implement it now
|
||||
param := routerPathParam{}
|
||||
if partExp == "*" {
|
||||
re = append(re, "(.*?)/?"...)
|
||||
if lastEnd < len(pattern) && pattern[lastEnd] == '/' {
|
||||
lastEnd++ // the "*" pattern is able to handle the last slash, so skip it
|
||||
}
|
||||
} else {
|
||||
partExp = util.IfZero(partExp, "[^/]+")
|
||||
re = append(re, '(')
|
||||
re = append(re, partExp...)
|
||||
re = append(re, ')')
|
||||
}
|
||||
param.name = partName
|
||||
p.params = append(p.params, param)
|
||||
}
|
||||
re = append(re, '$')
|
||||
reStr := string(re)
|
||||
p.re = regexp.MustCompile(reStr)
|
||||
return p
|
||||
}
|
@ -27,17 +27,21 @@ func chiURLParamsToMap(chiCtx *chi.Context) map[string]string {
|
||||
}
|
||||
m[key] = pathParams.Values[i]
|
||||
}
|
||||
return m
|
||||
return util.Iif(len(m) == 0, nil, m)
|
||||
}
|
||||
|
||||
func TestPathProcessor(t *testing.T) {
|
||||
testProcess := func(pattern, uri string, expectedPathParams map[string]string) {
|
||||
chiCtx := chi.NewRouteContext()
|
||||
chiCtx.RouteMethod = "GET"
|
||||
p := NewPathProcessor("GET", pattern)
|
||||
assert.True(t, p.ProcessRequestPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri)
|
||||
p := newRouterPathMatcher("GET", pattern, http.NotFound)
|
||||
assert.True(t, p.matchPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri)
|
||||
assert.Equal(t, expectedPathParams, chiURLParamsToMap(chiCtx), "use pattern %s to process uri %s", pattern, uri)
|
||||
}
|
||||
|
||||
// the "<...>" is intentionally designed to distinguish from chi's path parameters, because:
|
||||
// 1. their behaviors are totally different, we do not want to mislead developers
|
||||
// 2. we can write regexp in "<name:\w{3,4}>" easily and parse it easily
|
||||
testProcess("/<p1>/<p2>", "/a/b", map[string]string{"p1": "a", "p2": "b"})
|
||||
testProcess("/<p1:*>", "", map[string]string{"p1": ""}) // this is a special case, because chi router could use empty path
|
||||
testProcess("/<p1:*>", "/", map[string]string{"p1": ""})
|
||||
@ -67,24 +71,31 @@ func TestRouter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
stopMark := func(optMark ...string) func(resp http.ResponseWriter, req *http.Request) {
|
||||
mark := util.OptionalArg(optMark, "")
|
||||
return func(resp http.ResponseWriter, req *http.Request) {
|
||||
if stop := req.FormValue("stop"); stop != "" && (mark == "" || mark == stop) {
|
||||
h(stop)(resp, req)
|
||||
resp.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r := NewRouter()
|
||||
r.NotFound(h("not-found:/"))
|
||||
r.Get("/{username}/{reponame}/{type:issues|pulls}", h("list-issues-a")) // this one will never be called
|
||||
r.Group("/{username}/{reponame}", func() {
|
||||
r.Get("/{type:issues|pulls}", h("list-issues-b"))
|
||||
r.Group("", func() {
|
||||
r.Get("/{type:issues|pulls}/{index}", h("view-issue"))
|
||||
}, func(resp http.ResponseWriter, req *http.Request) {
|
||||
if stop := req.FormValue("stop"); stop != "" {
|
||||
h(stop)(resp, req)
|
||||
resp.WriteHeader(http.StatusOK)
|
||||
}
|
||||
})
|
||||
}, stopMark())
|
||||
r.Group("/issues/{index}", func() {
|
||||
r.Post("/update", h("update-issue"))
|
||||
})
|
||||
})
|
||||
|
||||
m := NewRouter()
|
||||
m.NotFound(h("not-found:/api/v1"))
|
||||
r.Mount("/api/v1", m)
|
||||
m.Group("/repos", func() {
|
||||
m.Group("/{username}/{reponame}", func() {
|
||||
@ -96,11 +107,14 @@ func TestRouter(t *testing.T) {
|
||||
m.Patch("", h())
|
||||
m.Delete("", h())
|
||||
})
|
||||
m.PathGroup("/*", func(g *RouterPathGroup) {
|
||||
g.MatchPath("GET", `/<dir:*>/<file:[a-z]{1,2}>`, stopMark("s2"), h("match-path"))
|
||||
}, stopMark("s1"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
testRoute := func(methodPath string, expected resultStruct) {
|
||||
testRoute := func(t *testing.T, methodPath string, expected resultStruct) {
|
||||
t.Run(methodPath, func(t *testing.T) {
|
||||
res = resultStruct{}
|
||||
methodPathFields := strings.Fields(methodPath)
|
||||
@ -111,24 +125,24 @@ func TestRouter(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Root Router", func(t *testing.T) {
|
||||
testRoute("GET /the-user/the-repo/other", resultStruct{})
|
||||
testRoute("GET /the-user/the-repo/pulls", resultStruct{
|
||||
t.Run("RootRouter", func(t *testing.T) {
|
||||
testRoute(t, "GET /the-user/the-repo/other", resultStruct{method: "GET", handlerMark: "not-found:/"})
|
||||
testRoute(t, "GET /the-user/the-repo/pulls", resultStruct{
|
||||
method: "GET",
|
||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"},
|
||||
handlerMark: "list-issues-b",
|
||||
})
|
||||
testRoute("GET /the-user/the-repo/issues/123", resultStruct{
|
||||
testRoute(t, "GET /the-user/the-repo/issues/123", resultStruct{
|
||||
method: "GET",
|
||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
|
||||
handlerMark: "view-issue",
|
||||
})
|
||||
testRoute("GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{
|
||||
testRoute(t, "GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{
|
||||
method: "GET",
|
||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"},
|
||||
handlerMark: "hijack",
|
||||
})
|
||||
testRoute("POST /the-user/the-repo/issues/123/update", resultStruct{
|
||||
testRoute(t, "POST /the-user/the-repo/issues/123/update", resultStruct{
|
||||
method: "POST",
|
||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"},
|
||||
handlerMark: "update-issue",
|
||||
@ -136,31 +150,57 @@ func TestRouter(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Sub Router", func(t *testing.T) {
|
||||
testRoute("GET /api/v1/repos/the-user/the-repo/branches", resultStruct{
|
||||
testRoute(t, "GET /api/v1/other", resultStruct{method: "GET", handlerMark: "not-found:/api/v1"})
|
||||
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches", resultStruct{
|
||||
method: "GET",
|
||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"},
|
||||
})
|
||||
|
||||
testRoute("POST /api/v1/repos/the-user/the-repo/branches", resultStruct{
|
||||
testRoute(t, "POST /api/v1/repos/the-user/the-repo/branches", resultStruct{
|
||||
method: "POST",
|
||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"},
|
||||
})
|
||||
|
||||
testRoute("GET /api/v1/repos/the-user/the-repo/branches/master", resultStruct{
|
||||
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/master", resultStruct{
|
||||
method: "GET",
|
||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"},
|
||||
})
|
||||
|
||||
testRoute("PATCH /api/v1/repos/the-user/the-repo/branches/master", resultStruct{
|
||||
testRoute(t, "PATCH /api/v1/repos/the-user/the-repo/branches/master", resultStruct{
|
||||
method: "PATCH",
|
||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"},
|
||||
})
|
||||
|
||||
testRoute("DELETE /api/v1/repos/the-user/the-repo/branches/master", resultStruct{
|
||||
testRoute(t, "DELETE /api/v1/repos/the-user/the-repo/branches/master", resultStruct{
|
||||
method: "DELETE",
|
||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "name": "master"},
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("MatchPath", func(t *testing.T) {
|
||||
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn", resultStruct{
|
||||
method: "GET",
|
||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"},
|
||||
handlerMark: "match-path",
|
||||
})
|
||||
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/000", resultStruct{
|
||||
method: "GET",
|
||||
pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"},
|
||||
handlerMark: "not-found:/api/v1",
|
||||
})
|
||||
|
||||
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s1", resultStruct{
|
||||
method: "GET",
|
||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn"},
|
||||
handlerMark: "s1",
|
||||
})
|
||||
|
||||
testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s2", resultStruct{
|
||||
method: "GET",
|
||||
pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"},
|
||||
handlerMark: "s2",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouteNormalizePath(t *testing.T) {
|
@ -647,6 +647,7 @@ joined_on=Přidal/a se %s
|
||||
repositories=Repozitáře
|
||||
activity=Veřejná aktivita
|
||||
followers=Sledující
|
||||
show_more=Zobrazit více
|
||||
starred=Oblíbené repozitáře
|
||||
watched=Sledované repozitáře
|
||||
code=Kód
|
||||
|
@ -93,6 +93,7 @@ remove_all=Alle entfernen
|
||||
remove_label_str=Element "%s " entfernen
|
||||
edit=Bearbeiten
|
||||
view=Anzeigen
|
||||
test=Test
|
||||
|
||||
enabled=Aktiviert
|
||||
disabled=Deaktiviert
|
||||
@ -103,6 +104,7 @@ copy_url=URL kopieren
|
||||
copy_hash=Hash kopieren
|
||||
copy_content=Inhalt kopieren
|
||||
copy_branch=Branchnamen kopieren
|
||||
copy_path=Pfad kopieren
|
||||
copy_success=Kopiert!
|
||||
copy_error=Kopieren fehlgeschlagen
|
||||
copy_type_unsupported=Dieser Dateityp kann nicht kopiert werden
|
||||
@ -143,6 +145,7 @@ confirm_delete_selected=Alle ausgewählten Elemente löschen?
|
||||
|
||||
name=Name
|
||||
value=Wert
|
||||
readme=Readme
|
||||
|
||||
filter=Filter
|
||||
filter.clear=Filter leeren
|
||||
@ -158,12 +161,15 @@ filter.public=Öffentlich
|
||||
filter.private=Privat
|
||||
|
||||
no_results_found=Es wurden keine Ergebnisse gefunden.
|
||||
internal_error_skipped=Ein interner Fehler ist aufgetreten, wurde aber übersprungen: %s
|
||||
|
||||
[search]
|
||||
search=Suche ...
|
||||
type_tooltip=Suchmodus
|
||||
fuzzy=Ähnlich
|
||||
fuzzy_tooltip=Ergebnisse einbeziehen, die dem Suchbegriff ähnlich sind
|
||||
exact=Exakt
|
||||
exact_tooltip=Nur Suchbegriffe einbeziehen, die dem exakten Suchbegriff entsprechen
|
||||
repo_kind=Repositories durchsuchen ...
|
||||
user_kind=Benutzer durchsuchen ...
|
||||
org_kind=Organisationen durchsuchen ...
|
||||
@ -174,9 +180,13 @@ code_search_by_git_grep=Aktuelle Code-Suchergebnisse werden von "git grep" berei
|
||||
package_kind=Pakete durchsuchen ...
|
||||
project_kind=Projekte durchsuchen ...
|
||||
branch_kind=Branches durchsuchen ...
|
||||
tag_kind=Tags durchsuchen...
|
||||
tag_tooltip=Suche nach passenden Tags. Benutze '%', um jede Sequenz von Zahlen zu treffen.
|
||||
commit_kind=Commits durchsuchen ...
|
||||
runner_kind=Runner durchsuchen ...
|
||||
no_results=Es wurden keine passenden Ergebnisse gefunden.
|
||||
issue_kind=Issues durchsuchen ...
|
||||
pull_kind=Pull-Requests durchsuchen...
|
||||
keyword_search_unavailable=Zurzeit ist die Stichwort-Suche nicht verfügbar. Bitte wende dich an den Website-Administrator.
|
||||
|
||||
[aria]
|
||||
@ -201,7 +211,10 @@ buttons.link.tooltip=Link hinzufügen
|
||||
buttons.list.unordered.tooltip=Liste hinzufügen
|
||||
buttons.list.ordered.tooltip=Nummerierte Liste hinzufügen
|
||||
buttons.list.task.tooltip=Aufgabenliste hinzufügen
|
||||
buttons.table.add.tooltip=Tabelle hinzufügen
|
||||
buttons.table.add.insert=Hinzufügen
|
||||
buttons.table.rows=Zeilen
|
||||
buttons.table.cols=Spalten
|
||||
buttons.mention.tooltip=Benutzer oder Team erwähnen
|
||||
buttons.ref.tooltip=Issue oder Pull-Request referenzieren
|
||||
buttons.switch_to_legacy.tooltip=Legacy-Editor verwenden
|
||||
@ -214,16 +227,20 @@ string.desc=Z–A
|
||||
|
||||
[error]
|
||||
occurred=Ein Fehler ist aufgetreten
|
||||
report_message=Wenn du glaubst, dass dies ein Fehler von Gitea ist, suche bitte auf <a href="%s" target="_blank">GitHub</a> nach diesem Fehler und erstelle gegebenenfalls einen neuen Bugreport.
|
||||
not_found=Das Ziel konnte nicht gefunden werden.
|
||||
network_error=Netzwerkfehler
|
||||
|
||||
[startpage]
|
||||
app_desc=Ein einfacher, selbst gehosteter Git-Service
|
||||
install=Einfach zu installieren
|
||||
install_desc=Starte einfach <a target="_blank" rel="noopener noreferrer" href="%[1]s">die Anwendung</a> für deine Plattform oder nutze <a target="_blank" rel="noopener noreferrer" href="%[2]s">Docker</a>. Es existieren auch <a target="_blank" rel="noopener noreferrer" href="%[3]s">paketierte Versionen</a>.
|
||||
platform=Plattformübergreifend
|
||||
platform_desc=Gitea läuft überall, wo <a target="_blank" rel="noopener noreferrer" href="%s">Go</a> kompiliert: Windows, macOS, Linux, ARM, etc. Wähle das System, das dir am meisten gefällt!
|
||||
lightweight=Leichtgewicht
|
||||
lightweight_desc=Gitea hat minimale Systemanforderungen und kann selbst auf einem günstigen und stromsparenden Raspberry Pi betrieben werden!
|
||||
license=Quelloffen
|
||||
license_desc=Hol dir den Code unter <a target="_blank" rel="noopener noreferrer" href="%[1]s">%[2]s</a>! Leiste deinen <a target="_blank" rel="noopener noreferrer" href="%[3]s">Beitrag</a> bei der Verbesserung dieses Projekts. Trau dich!
|
||||
|
||||
[install]
|
||||
install=Installation
|
||||
@ -337,6 +354,7 @@ enable_update_checker=Aktualisierungsprüfung aktivieren
|
||||
enable_update_checker_helper=Stellt regelmäßig eine Verbindung zu gitea.io her, um nach neuen Versionen zu prüfen.
|
||||
env_config_keys=Umgebungskonfiguration
|
||||
env_config_keys_prompt=Die folgenden Umgebungsvariablen werden auch auf Ihre Konfigurationsdatei angewendet:
|
||||
config_write_file_prompt=Diese Konfigurationsoptionen werden in %s geschrieben
|
||||
|
||||
[home]
|
||||
nav_menu=Navigationsmenü
|
||||
@ -377,6 +395,8 @@ relevant_repositories=Es werden nur relevante Repositories angezeigt, <a href="%
|
||||
|
||||
[auth]
|
||||
create_new_account=Konto anlegen
|
||||
already_have_account=Du hast bereits ein Konto?
|
||||
sign_in_now=Jetzt anmelden!
|
||||
disable_register_prompt=Die Registrierung ist deaktiviert. Bitte wende dich an den Administrator.
|
||||
disable_register_mail=E-Mail-Bestätigung bei der Registrierung ist deaktiviert.
|
||||
manual_activation_only=Kontaktiere den Website-Administrator, um die Aktivierung abzuschließen.
|
||||
@ -384,6 +404,8 @@ remember_me=Dieses Gerät speichern
|
||||
remember_me.compromised=Das Login-Token ist nicht mehr gültig, was auf ein kompromittiertes Konto hindeuten kann. Bitte überprüfe dein Konto auf ungewöhnliche Aktivitäten.
|
||||
forgot_password_title=Passwort vergessen
|
||||
forgot_password=Passwort vergessen?
|
||||
need_account=Noch kein Konto?
|
||||
sign_up_now=Jetzt registrieren.
|
||||
sign_up_successful=Konto wurde erfolgreich erstellt. Willkommen!
|
||||
confirmation_mail_sent_prompt_ex=Eine neue Bestätigungs-E-Mail wurde an <b>%s</b>gesendet. Bitte überprüfe deinen Posteingang innerhalb der nächsten %s, um den Registrierungsprozess abzuschließen. Wenn deine Registrierungs-E-Mail-Adresse falsch ist, kannst du dich erneut anmelden und diese ändern.
|
||||
must_change_password=Aktualisiere dein Passwort
|
||||
@ -424,6 +446,7 @@ oauth_signin_submit=Konto verbinden
|
||||
oauth.signin.error=Beim Verarbeiten der Autorisierungsanfrage ist ein Fehler aufgetreten. Wenn dieser Fehler weiterhin besteht, wende dich bitte an deinen Administrator.
|
||||
oauth.signin.error.access_denied=Die Autorisierungsanfrage wurde abgelehnt.
|
||||
oauth.signin.error.temporarily_unavailable=Autorisierung fehlgeschlagen, da der Authentifizierungsserver vorübergehend nicht verfügbar ist. Bitte versuch es später erneut.
|
||||
oauth_callback_unable_auto_reg=Automatische Registrierung ist aktiviert, aber der OAuth2-Provider %[1]s hat fehlende Felder zurückgegeben: %[2]s, kann den Account nicht automatisch erstellen. Bitte erstelle oder verbinde einen Account oder kontaktieren den Administrator.
|
||||
openid_connect_submit=Verbinden
|
||||
openid_connect_title=Mit bestehendem Konto verbinden
|
||||
openid_connect_desc=Die gewählte OpenID-URI ist unbekannt. Ordne sie hier einem neuen Account zu.
|
||||
@ -437,12 +460,16 @@ authorize_application=Anwendung autorisieren
|
||||
authorize_redirect_notice=Du wirst zu %s weitergeleitet, wenn du diese Anwendung autorisierst.
|
||||
authorize_application_created_by=Diese Anwendung wurde von %s erstellt.
|
||||
authorize_application_description=Wenn du diese Anwendung autorisierst, wird sie die Berechtigung erhalten, alle Informationen zu deinem Account zu bearbeiten oder zu lesen. Dies beinhaltet auch private Repositories und Organisationen.
|
||||
authorize_application_with_scopes=Mit Bereichen: %s
|
||||
authorize_title=`"%s" den Zugriff auf deinen Account gestatten?`
|
||||
authorization_failed=Autorisierung fehlgeschlagen
|
||||
authorization_failed_desc=Die Autorisierung ist fehlgeschlagen, da wir eine ungültige Anfrage erkannt haben. Bitte kontaktiere den Betreuer der App, die du zu autorisieren versucht hast.
|
||||
sspi_auth_failed=SSPI-Authentifizierung fehlgeschlagen
|
||||
password_pwned=Das von dir gewählte Passwort befindet sich auf einer <a target="_blank" rel="noopener noreferrer" href="%s">Liste gestohlener Passwörter</a>, die öffentlich verfügbar sind. Bitte versuche es erneut mit einem anderen Passwort und ziehe in Erwägung, auch anderswo deine Passwörter zu ändern.
|
||||
password_pwned_err=Anfrage an HaveIBeenPwned konnte nicht abgeschlossen werden
|
||||
last_admin=Du kannst den letzten Admin nicht entfernen. Es muss mindestens einen Administrator geben.
|
||||
signin_passkey=Mit einem Passkey anmelden
|
||||
back_to_sign_in=Zurück zum Anmelden
|
||||
|
||||
[mail]
|
||||
view_it_on=Auf %s ansehen
|
||||
@ -459,6 +486,7 @@ activate_email=Bestätige deine E-Mail-Adresse
|
||||
activate_email.title=%s, bitte verifiziere deine E-Mail-Adresse
|
||||
activate_email.text=Bitte klicke innerhalb von <b>%s</b> auf folgenden Link, um dein Konto zu aktivieren:
|
||||
|
||||
register_notify=Willkommen bei %s
|
||||
register_notify.title=%[1]s, willkommen bei %[2]s
|
||||
register_notify.text_1=dies ist deine Bestätigungs-E-Mail für %s!
|
||||
register_notify.text_2=Du kannst dich jetzt mit dem Benutzernamen "%s" anmelden.
|
||||
@ -560,6 +588,8 @@ lang_select_error=Wähle eine Sprache aus der Liste aus.
|
||||
|
||||
username_been_taken=Der Benutzername ist bereits vergeben.
|
||||
username_change_not_local_user=Nicht-lokale Benutzer dürfen ihren Nutzernamen nicht ändern.
|
||||
change_username_disabled=Ändern des Benutzernamens ist deaktiviert.
|
||||
change_full_name_disabled=Ändern des vollständigen Namens ist deaktiviert.
|
||||
username_has_not_been_changed=Benutzername wurde nicht geändert
|
||||
repo_name_been_taken=Der Repository-Name wird schon verwendet.
|
||||
repository_force_private=Privat erzwingen ist aktiviert: Private Repositories können nicht veröffentlicht werden.
|
||||
@ -609,6 +639,7 @@ org_still_own_repo=Diese Organisation besitzt noch ein oder mehrere Repositories
|
||||
org_still_own_packages=Diese Organisation besitzt noch ein oder mehrere Pakete, lösche diese zuerst.
|
||||
|
||||
target_branch_not_exist=Der Ziel-Branch existiert nicht.
|
||||
target_ref_not_exist=Zielreferenz existiert nicht %s
|
||||
|
||||
admin_cannot_delete_self=Du kannst dich nicht selbst löschen, wenn du ein Administrator bist. Bitte entferne zuerst deine Administratorrechte.
|
||||
|
||||
@ -618,6 +649,7 @@ joined_on=Beigetreten am %s
|
||||
repositories=Repositories
|
||||
activity=Öffentliche Aktivität
|
||||
followers=Follower
|
||||
show_more=Mehr anzeigen
|
||||
starred=Favoriten
|
||||
watched=Beobachtete Repositories
|
||||
code=Quelltext
|
||||
@ -684,6 +716,8 @@ public_profile=Öffentliches Profil
|
||||
biography_placeholder=Erzähle uns ein wenig über Dich selbst! (Du kannst Markdown verwenden)
|
||||
location_placeholder=Teile Deinen ungefähren Standort mit anderen
|
||||
profile_desc=Lege fest, wie dein Profil anderen Benutzern angezeigt wird. Deine primäre E-Mail-Adresse wird für Benachrichtigungen, Passwort-Wiederherstellung und webbasierte Git-Operationen verwendet.
|
||||
password_username_disabled=Du bist nicht berechtigt, den Benutzernamen zu ändern. Bitte kontaktiere Deinen Seitenadministrator für weitere Details.
|
||||
password_full_name_disabled=Du bist nicht berechtigt, den vollständigen Namen zu ändern. Bitte kontaktiere Deinen Seitenadministrator für weitere Details.
|
||||
full_name=Vollständiger Name
|
||||
website=Webseite
|
||||
location=Standort
|
||||
@ -701,6 +735,7 @@ cancel=Abbrechen
|
||||
language=Sprache
|
||||
ui=Theme
|
||||
hidden_comment_types=Ausgeblendeter Kommentartypen
|
||||
hidden_comment_types_description=Die hier markierten Kommentartypen werden nicht innerhalb der Issue-Seiten angezeigt. Beispielsweise entfernt das Markieren von "Label" alle "{user} hat {label} hinzugefügt/entfernt"-Kommentare.
|
||||
hidden_comment_types.ref_tooltip=Kommentare, in denen dieses Issue von einem anderen Issue/Commit referenziert wurde
|
||||
hidden_comment_types.issue_ref_tooltip=Kommentare, bei denen der Benutzer den Branch/Tag des Issues ändert
|
||||
comment_type_group_reference=Verweis auf Mitglieder
|
||||
@ -732,6 +767,7 @@ uploaded_avatar_not_a_image=Die hochgeladene Datei ist kein Bild.
|
||||
uploaded_avatar_is_too_big=Die hochgeladene Dateigröße (%d KiB) überschreitet die maximale Größe (%d KiB).
|
||||
update_avatar_success=Dein Profilbild wurde geändert.
|
||||
update_user_avatar_success=Der Avatar des Benutzers wurde aktualisiert.
|
||||
cropper_prompt=Sie können das Bild vor dem Speichern bearbeiten. Das bearbeitete Bild wird als PNG-Datei gespeichert.
|
||||
|
||||
change_password=Passwort aktualisieren
|
||||
old_password=Aktuelles Passwort
|
||||
@ -747,6 +783,8 @@ manage_themes=Standard-Theme auswählen
|
||||
manage_openid=OpenID-Adressen verwalten
|
||||
email_desc=Deine primäre E-Mail-Adresse wird für Benachrichtigungen, Passwort-Wiederherstellung und, sofern sie nicht versteckt ist, web-basierte Git-Operationen verwendet.
|
||||
theme_desc=Dies wird dein Standard-Theme auf der Seite sein.
|
||||
theme_colorblindness_help=Hilfe zum Theme für Farbenblinde
|
||||
theme_colorblindness_prompt=Gitea erhält aktuell einfache Unterstützung für Farbenblinde durch einige Themes, die nur wenige Farben definiert haben. Die Arbeit ist noch im Gange. Weitere Verbesserungen können durch die Definition von mehr Farben in den CSS-Theme-Dateien vorgenommen werden.
|
||||
primary=Primär
|
||||
activated=Aktiviert
|
||||
requires_activation=Erfordert Aktivierung
|
||||
@ -871,8 +909,9 @@ repo_and_org_access=Repository- und Organisationszugriff
|
||||
permissions_public_only=Nur öffentlich
|
||||
permissions_access_all=Alle (öffentlich, privat und begrenzt)
|
||||
select_permissions=Berechtigungen auswählen
|
||||
permission_not_set=Nicht festgelegt
|
||||
permission_no_access=Kein Zugriff
|
||||
permission_read=Gelesen
|
||||
permission_read=Lesen
|
||||
permission_write=Lesen und Schreiben
|
||||
access_token_desc=Ausgewählte Token-Berechtigungen beschränken die Authentifizierung auf die entsprechenden <a %s>API</a>-Routen. Lies die <a %s>Dokumentation</a> für mehr Informationen.
|
||||
at_least_one_permission=Du musst mindestens eine Berechtigung auswählen, um ein Token zu erstellen
|
||||
@ -890,6 +929,7 @@ create_oauth2_application_success=Du hast erfolgreich eine neue OAuth2-Anwendung
|
||||
update_oauth2_application_success=Du hast die OAuth2-Anwendung erfolgreich aktualisiert.
|
||||
oauth2_application_name=Name der Anwendung
|
||||
oauth2_confidential_client=Vertraulicher Client. Für Anwendungen aktivieren, die das Geheimnis sicher speichern, z. B. Webanwendungen. Wähle diese Option nicht für native Anwendungen für PCs und Mobilgeräte.
|
||||
oauth2_skip_secondary_authorization=Autorisierung für öffentliche Clients nach einmaliger Gewährung des Zugriffs überspringen. <strong>Dies kann ein Sicherheitsrisiko darstellen.</strong>
|
||||
oauth2_redirect_uris=URIs für die Weiterleitung. Bitte verwende eine neue Zeile für jede URI.
|
||||
save_application=Speichern
|
||||
oauth2_client_id=Client-ID
|
||||
@ -900,6 +940,7 @@ oauth2_client_secret_hint=Das Secret wird nach dem Verlassen oder Aktualisieren
|
||||
oauth2_application_edit=Bearbeiten
|
||||
oauth2_application_create_description=OAuth2 Anwendungen geben deiner Drittanwendung Zugriff auf Benutzeraccounts dieser Gitea-Instanz.
|
||||
oauth2_application_remove_description=Das Entfernen einer OAuth2-Anwendung hat zur Folge, dass diese nicht mehr auf autorisierte Benutzeraccounts auf dieser Instanz zugreifen kann. Möchtest Du fortfahren?
|
||||
oauth2_application_locked=Wenn es in der Konfiguration aktiviert ist, registriert Gitea einige OAuth2-Anwendungen beim Starten vor. Um unerwartetes Verhalten zu verhindern, können diese weder bearbeitet noch entfernt werden. Weitere Informationen findest Du in der OAuth2-Dokumentation.
|
||||
|
||||
authorized_oauth2_applications=Autorisierte OAuth2-Anwendungen
|
||||
authorized_oauth2_applications_description=Den folgenden Drittanbieter-Apps hast Du Zugriff auf Deinen persönlichen Gitea-Account gewährt. Bitte widerrufe die Autorisierung für Apps, die Du nicht mehr nutzt.
|
||||
@ -908,20 +949,26 @@ revoke_oauth2_grant=Autorisierung widerrufen
|
||||
revoke_oauth2_grant_description=Wenn du die Autorisierung widerrufst, kann die Anwendung nicht mehr auf deine Daten zugreifen. Bist du dir sicher?
|
||||
revoke_oauth2_grant_success=Zugriff erfolgreich widerrufen.
|
||||
|
||||
twofa_desc=Um dein Konto vor Passwortdiebstahl zu schützen, kannst du ein Smartphone oder ein anderes Gerät verwenden, um zeitbasierte Einmalpasswörter ("TOTP") zu erhalten.
|
||||
twofa_recovery_tip=Wenn du dein Gerät verlierst, kannst du einen einmalig verwendbaren Wiederherstellungsschlüssel nutzen, um den Zugriff auf dein Konto wiederherzustellen.
|
||||
twofa_is_enrolled=Für dein Konto ist die Zwei-Faktor-Authentifizierung <strong>eingeschaltet</strong>.
|
||||
twofa_not_enrolled=Für dein Konto ist die Zwei-Faktor-Authentifizierung momentan nicht eingeschaltet.
|
||||
twofa_disable=Zwei-Faktor-Authentifizierung deaktivieren
|
||||
twofa_scratch_token_regenerate=Einweg-Wiederherstellungsschlüssel neu generieren
|
||||
twofa_scratch_token_regenerated=Dein Einweg-Wiederherstellungsschlüssel ist jetzt %s. Speichere ihn an einem sicheren Ort, er wird nie wieder angezeigt.
|
||||
twofa_enroll=Zwei-Faktor-Authentifizierung aktivieren
|
||||
twofa_disable_note=Du kannst die Zwei-Faktor-Authentifizierung auch wieder deaktivieren.
|
||||
twofa_disable_desc=Wenn du die Zwei-Faktor-Authentifizierung deaktivierst, wird die Sicherheit deines Kontos verringert. Fortfahren?
|
||||
regenerate_scratch_token_desc=Wenn du deinen Wiederherstellungsschlüssel verlegt oder bereits benutzt hast, kannst du ihn hier zurücksetzen.
|
||||
twofa_disabled=Zwei-Faktor-Authentifizierung wurde deaktiviert.
|
||||
scan_this_image=Scanne diese Grafik mit deiner Authentifizierungs-App:
|
||||
or_enter_secret=Oder gib das Secret ein: %s
|
||||
then_enter_passcode=Und gebe dann die angezeigte PIN der Anwendung ein:
|
||||
passcode_invalid=Die PIN ist falsch. Probiere es erneut.
|
||||
twofa_enrolled=Die Zwei-Faktor-Authentifizierung wurde für dein Konto aktiviert. Bewahre deinen Einweg-Wiederherstellungsschlüssel (%s) an einem sicheren Ort auf, da er nicht wieder angezeigt werden wird.
|
||||
twofa_failed_get_secret=Fehler beim Abrufen des Secrets.
|
||||
|
||||
webauthn_desc=Sicherheitsschlüssel sind Geräte, die kryptografische Schlüssel beeinhalten. Diese können für die Zwei-Faktor-Authentifizierung verwendet werden. Der Sicherheitsschlüssel muss den Standard "<a rel="noreferrer" target="_blank" href="%s">WebAuthn</a>" unterstützen.
|
||||
webauthn_register_key=Sicherheitsschlüssel hinzufügen
|
||||
webauthn_nickname=Nickname
|
||||
webauthn_delete_key=Sicherheitsschlüssel entfernen
|
||||
@ -987,6 +1034,8 @@ fork_to_different_account=Fork in ein anderes Konto erstellen
|
||||
fork_visibility_helper=Die Sichtbarkeit eines geforkten Repositories kann nicht geändert werden.
|
||||
fork_branch=Branch, der zum Fork geklont werden soll
|
||||
all_branches=Alle Branches
|
||||
view_all_branches=Alle Branches anzeigen
|
||||
view_all_tags=Alle Tags anzeigen
|
||||
fork_no_valid_owners=Dieses Repository kann nicht geforkt werden, da keine gültigen Besitzer vorhanden sind.
|
||||
fork.blocked_user=Das Repository kann nicht geforkt werden, da du vom Repository-Eigentümer blockiert wurdest.
|
||||
use_template=Dieses Template verwenden
|
||||
@ -998,6 +1047,8 @@ generate_repo=Repository erstellen
|
||||
generate_from=Erstelle aus
|
||||
repo_desc=Beschreibung
|
||||
repo_desc_helper=Gib eine kurze Beschreibung an (optional)
|
||||
repo_no_desc=Keine Beschreibung vorhanden
|
||||
repo_lang=Sprachen
|
||||
repo_gitignore_helper=Wähle eine .gitignore-Vorlage aus.
|
||||
repo_gitignore_helper_desc=Wähle aus einer Liste an Vorlagen für bekannte Sprachen, welche Dateien ignoriert werden sollen. Typische Artefakte, die durch die Build Tools der gewählten Sprache generiert werden, sind standardmäßig Bestandteil der .gitignore.
|
||||
issue_labels=Issue Label
|
||||
@ -1005,6 +1056,7 @@ issue_labels_helper=Wähle ein Issue-Label-Set.
|
||||
license=Lizenz
|
||||
license_helper=Wähle eine Lizenz aus.
|
||||
license_helper_desc=Eine Lizenz regelt, was Andere mit deinem Code (nicht) tun können. Unsicher, welches für dein Projekt die Richtige ist? Siehe <a target="_blank" rel="noopener noreferrer" href="%s">eine Lizenz wählen</a>.
|
||||
multiple_licenses=Mehrere Lizenzen
|
||||
object_format=Objektformat
|
||||
object_format_helper=Objektformat des Repositories. Es kann später nicht geändert werden. SHA1 ist am meisten kompatibel.
|
||||
readme=README
|
||||
@ -1058,13 +1110,16 @@ delete_preexisting_success=Nicht übernommene Dateien in %s gelöscht
|
||||
blame_prior=Blame vor dieser Änderung anzeigen
|
||||
blame.ignore_revs=Revisionen in <a href="%s">.git-blame-ignore-revs</a> werden ignoriert. Klicke <a href="%s">hier, um das zu umgehen</a> und die normale Blame-Ansicht zu sehen.
|
||||
blame.ignore_revs.failed=Fehler beim Ignorieren der Revisionen in <a href="%s">.git-blame-ignore-revs</a>.
|
||||
user_search_tooltip=Zeigt maximal 30 Benutzer
|
||||
|
||||
tree_path_not_found_commit=Pfad %[1]s existiert nicht in Commit%[2]s
|
||||
tree_path_not_found_branch=Pfad %[1]s existiert nicht in Branch %[2]s
|
||||
tree_path_not_found_tag=Pfad %[1]s existiert nicht in Tag %[2]s
|
||||
|
||||
transfer.accept=Übertragung Akzeptieren
|
||||
transfer.accept_desc=`Übertragung nach "%s"`
|
||||
transfer.reject=Übertragung Ablehnen
|
||||
transfer.reject_desc=Übertragung nach "%s " abbrechen
|
||||
transfer.no_permission_to_accept=Du hast keine Berechtigung, diesen Transfer anzunehmen.
|
||||
transfer.no_permission_to_reject=Du hast keine Berechtigung, diesen Transfer abzulehnen.
|
||||
|
||||
@ -1139,6 +1194,11 @@ migrate.gogs.description=Daten von notabug.org oder anderen Gogs Instanzen migri
|
||||
migrate.onedev.description=Daten von code.onedev.io oder anderen OneDev Instanzen migrieren.
|
||||
migrate.codebase.description=Daten von codebasehq.com migrieren.
|
||||
migrate.gitbucket.description=Daten von GitBucket Instanzen migrieren.
|
||||
migrate.codecommit.description=Daten von AWS CodeCommit migrieren.
|
||||
migrate.codecommit.aws_access_key_id=AWS Access Key ID
|
||||
migrate.codecommit.aws_secret_access_key=AWS Secret Access Key
|
||||
migrate.codecommit.https_git_credentials_username=HTTPS-Git-Nutzername
|
||||
migrate.codecommit.https_git_credentials_password=HTTPS-Git-Passwort
|
||||
migrate.migrating_git=Git-Daten werden migriert
|
||||
migrate.migrating_topics=Themen werden migriert
|
||||
migrate.migrating_milestones=Meilensteine werden migriert
|
||||
@ -1199,6 +1259,7 @@ releases=Releases
|
||||
tag=Tag
|
||||
released_this=hat released
|
||||
tagged_this=hat getaggt
|
||||
file.title=%s in %s
|
||||
file_raw=Originalformat
|
||||
file_history=Verlauf
|
||||
file_view_source=Quelltext anzeigen
|
||||
@ -1206,12 +1267,16 @@ file_view_rendered=Ansicht rendern
|
||||
file_view_raw=Originalformat anzeigen
|
||||
file_permalink=Permalink
|
||||
file_too_large=Die Datei ist zu groß zum Anzeigen.
|
||||
file_is_empty=Die Datei ist leer.
|
||||
code_preview_line_from_to=Zeilen %[1]d bis %[2]d in %[3]s
|
||||
code_preview_line_in=Zeile %[1]d in %[2]s
|
||||
invisible_runes_header=`Diese Datei enthält unsichtbare Unicode-Zeichen`
|
||||
invisible_runes_description=`Diese Datei enthält unsichtbare Unicode-Zeichen, die für Menschen nicht unterscheidbar sind, aber von einem Computer unterschiedlich verarbeitet werden können. Wenn du glaubst, dass das absichtlich so ist, kannst du diese Warnung ignorieren. Benutze den „Escape“-Button, um versteckte Zeichen anzuzeigen.`
|
||||
ambiguous_runes_header=`Diese Datei enthält mehrdeutige Unicode-Zeichen`
|
||||
ambiguous_runes_description=`Diese Datei enthält Unicode-Zeichen, die mit anderen Zeichen verwechselt werden können. Wenn du glaubst, dass das absichtlich so ist, kannst du diese Warnung ignorieren. Benutze den „Escape“-Button, um versteckte Zeichen anzuzeigen.`
|
||||
invisible_runes_line=`Diese Zeile enthält unsichtbare Unicode-Zeichen`
|
||||
ambiguous_runes_line=`Diese Zeile enthält mehrdeutige Unicode-Zeichen`
|
||||
ambiguous_character=`%[1]c [U+%04[1]X] kann mit %[2]c [U+%04[2]X] verwechselt werden`
|
||||
|
||||
escape_control_characters=Escapen
|
||||
unescape_control_characters=Unescapen
|
||||
@ -1259,6 +1324,7 @@ editor.or=oder
|
||||
editor.cancel_lower=Abbrechen
|
||||
editor.commit_signed_changes=Committe signierte Änderungen
|
||||
editor.commit_changes=Änderungen committen
|
||||
editor.add_tmpl='{filename}' hinzufügen
|
||||
editor.add=%s hinzugefügt
|
||||
editor.update=%s aktualisiert
|
||||
editor.delete=%s gelöscht
|
||||
@ -1342,6 +1408,7 @@ commitstatus.success=Erfolg
|
||||
ext_issues=Zugriff auf Externe Issues
|
||||
ext_issues.desc=Link zu externem Issuetracker.
|
||||
|
||||
projects.desc=Verwalte Issues und Pull-Requests in Projekten.
|
||||
projects.description=Beschreibung (optional)
|
||||
projects.description_placeholder=Beschreibung
|
||||
projects.create=Projekt erstellen
|
||||
@ -1401,7 +1468,9 @@ issues.new.clear_milestone=Meilenstein entfernen
|
||||
issues.new.assignees=Zuständig
|
||||
issues.new.clear_assignees=Zuständige entfernen
|
||||
issues.new.no_assignees=Niemand zuständig
|
||||
issues.new.no_reviewers=Keine Reviewer
|
||||
issues.new.blocked_user=Das Issue kann nicht erstellt werden, da du vom Repository-Eigentümer blockiert wurdest.
|
||||
issues.edit.already_changed=Änderungen zum Issue konnten nicht gespeichert werden. Es scheint, dass der Inhalt bereits von einem anderen Benutzer geändert wurde. Bitte aktualisiere die Seite und bearbeite diese erneut, um zu verhindern, dass die Änderungen des anderen Benutzers überschrieben werden
|
||||
issues.edit.blocked_user=Der Inhalt kann nicht bearbeitet werden, da du vom Repository-Eigentümer blockiert wurdest.
|
||||
issues.choose.get_started=Los geht's
|
||||
issues.choose.open_external_link=Öffnen
|
||||
@ -1428,6 +1497,7 @@ issues.remove_labels=hat die Labels %s %s entfernt
|
||||
issues.add_remove_labels=hat %s hinzugefügt, und %s %s entfernt
|
||||
issues.add_milestone_at=`hat diesen Issue %[2]s zum <b>%[1]s</b> Meilenstein hinzugefügt`
|
||||
issues.add_project_at=`hat dieses zum <b>%s</b> projekt %s hinzugefügt`
|
||||
issues.move_to_column_of_project=`hat dies zu %s in %s %s verschoben`
|
||||
issues.change_milestone_at=`hat den Meilenstein %[3]s von <b>%[1]s</b> zu <b>%[2]s</b> geändert`
|
||||
issues.change_project_at=`hat das Projekt %[3]s von <b>%[1]s</b> zu <b>%[2]s</b> geändert`
|
||||
issues.remove_milestone_at=`hat dieses Issue %[2]s vom <b>%[1]s</b> Meilenstein entfernt`
|
||||
@ -1459,6 +1529,8 @@ issues.filter_assignee=Zuständig
|
||||
issues.filter_assginee_no_select=Alle Zuständigen
|
||||
issues.filter_assginee_no_assignee=Niemand zuständig
|
||||
issues.filter_poster=Autor
|
||||
issues.filter_user_placeholder=Benutzer suchen
|
||||
issues.filter_user_no_select=Alle Benutzer
|
||||
issues.filter_type=Typ
|
||||
issues.filter_type.all_issues=Alle Issues
|
||||
issues.filter_type.assigned_to_you=Dir zugewiesen
|
||||
@ -1512,7 +1584,9 @@ issues.no_content=Keine Beschreibung angegeben.
|
||||
issues.close=Issue schließen
|
||||
issues.comment_pull_merged_at=hat Commit %[1]s in %[2]s %[3]s gemerged
|
||||
issues.comment_manually_pull_merged_at=hat Commit %[1]s in %[2]s %[3]s manuell gemerged
|
||||
issues.close_comment_issue=Kommentieren und schließen
|
||||
issues.reopen_issue=Wieder öffnen
|
||||
issues.reopen_comment_issue=Kommentieren und wieder öffnen
|
||||
issues.create_comment=Kommentieren
|
||||
issues.comment.blocked_user=Der Kommentar kann nicht erstellt oder bearbeitet werden, da du vom Repository-Eigentümer blockiert wurdest.
|
||||
issues.closed_at=`hat diesen Issue <a id="%[1]s" href="#%[1]s">%[2]s</a> geschlossen`
|
||||
@ -1601,12 +1675,25 @@ issues.delete.title=Dieses Issue löschen?
|
||||
issues.delete.text=Möchtest du dieses Issue wirklich löschen? (Dadurch wird der Inhalt dauerhaft gelöscht. Denke daran, es stattdessen zu schließen, wenn du es archivieren willst)
|
||||
|
||||
issues.tracker=Zeiterfassung
|
||||
issues.timetracker_timer_start=Timer starten
|
||||
issues.timetracker_timer_stop=Timer stoppen
|
||||
issues.timetracker_timer_discard=Timer verwerfen
|
||||
issues.timetracker_timer_manually_add=Zeit hinzufügen
|
||||
|
||||
issues.time_estimate_set=Geschätzte Zeit festlegen
|
||||
issues.time_estimate_display=Schätzung: %s
|
||||
issues.change_time_estimate_at=Zeitschätzung geändert zu <b>%s</b> %s
|
||||
issues.remove_time_estimate_at=Zeitschätzung %s entfernt
|
||||
issues.time_estimate_invalid=Format der Zeitschätzung ist ungültig
|
||||
issues.start_tracking_history=hat die Zeiterfassung %s gestartet
|
||||
issues.tracker_auto_close=Der Timer wird automatisch gestoppt, wenn dieser Issue geschlossen wird
|
||||
issues.tracking_already_started=`Du hast die Zeiterfassung bereits in <a href="%s">diesem Issue</a> gestartet!`
|
||||
issues.stop_tracking_history=hat für <b>%s</b> gearbeitet %s
|
||||
issues.cancel_tracking_history=`hat die Zeiterfassung %s abgebrochen`
|
||||
issues.del_time=Diese Zeiterfassung löschen
|
||||
issues.add_time_history=hat <b>%s</b> gearbeitete Zeit hinzugefügt %s
|
||||
issues.del_time_history=`hat %s gearbeitete Zeit gelöscht`
|
||||
issues.add_time_manually=Zeit manuell hinzufügen
|
||||
issues.add_time_hours=Stunden
|
||||
issues.add_time_minutes=Minuten
|
||||
issues.add_time_sum_to_small=Es wurde keine Zeit eingegeben.
|
||||
@ -1666,6 +1753,7 @@ issues.dependency.add_error_dep_not_same_repo=Beide Issues müssen sich im selbe
|
||||
issues.review.self.approval=Du kannst nicht dein eigenen Pull-Request genehmigen.
|
||||
issues.review.self.rejection=Du kannst keine Änderungen an deinem eigenen Pull-Request anfragen.
|
||||
issues.review.approve=hat die Änderungen %s genehmigt
|
||||
issues.review.comment=hat %s überprüft
|
||||
issues.review.dismissed=verwarf %ss Review %s
|
||||
issues.review.dismissed_label=Verworfen
|
||||
issues.review.left_comment=hat einen Kommentar hinterlassen
|
||||
@ -1691,6 +1779,11 @@ issues.review.resolve_conversation=Diskussion als "erledigt" markieren
|
||||
issues.review.un_resolve_conversation=Diskussion als "nicht-erledigt" markieren
|
||||
issues.review.resolved_by=markierte diese Unterhaltung als gelöst
|
||||
issues.review.commented=Kommentieren
|
||||
issues.review.official=Genehmigt
|
||||
issues.review.requested=Prüfung ausstehend
|
||||
issues.review.rejected=Änderungen angefordert
|
||||
issues.review.stale=Aktualisiert seit der Genehmigung
|
||||
issues.review.unofficial=Ungezählte Genehmigung
|
||||
issues.assignee.error=Aufgrund eines unerwarteten Fehlers konnten nicht alle Beauftragten hinzugefügt werden.
|
||||
issues.reference_issue.body=Beschreibung
|
||||
issues.content_history.deleted=gelöscht
|
||||
@ -1707,6 +1800,8 @@ compare.compare_head=vergleichen
|
||||
pulls.desc=Pull-Requests und Code-Reviews aktivieren.
|
||||
pulls.new=Neuer Pull-Request
|
||||
pulls.new.blocked_user=Der Pull Request kann nicht erstellt werden, da du vom Repository-Eigentümer blockiert wurdest.
|
||||
pulls.new.must_collaborator=Du musst Mitarbeiter sein, um Pull-Requests zu erstellen.
|
||||
pulls.edit.already_changed=Änderungen zum Pull-Request konnten nicht gespeichert werden. Es scheint, dass der Inhalt bereits von einem anderen Benutzer geändert wurde. Bitte aktualisieren die Seite und bearbeite diesen erneut, um zu verhindern, dass die Änderungen des anderen Benutzers überschrieben werden
|
||||
pulls.view=Pull-Request ansehen
|
||||
pulls.compare_changes=Neuer Pull-Request
|
||||
pulls.allow_edits_from_maintainers=Änderungen von Maintainern erlauben
|
||||
@ -1762,6 +1857,8 @@ pulls.is_empty=Die Änderungen an diesem Branch sind bereits auf dem Zielbranch.
|
||||
pulls.required_status_check_failed=Einige erforderliche Prüfungen waren nicht erfolgreich.
|
||||
pulls.required_status_check_missing=Einige erforderliche Prüfungen fehlen.
|
||||
pulls.required_status_check_administrator=Als Administrator kannst du diesen Pull-Request weiterhin mergen.
|
||||
pulls.blocked_by_approvals=Dieser Pull-Request hat noch nicht genügend Genehmigungen. %d von %d Genehmigungen erteilt.
|
||||
pulls.blocked_by_approvals_whitelisted=Dieser Pull-Request hat noch nicht genug erforderliche Genehmigungen. %d von %d Genehmigungen von Benutzern oder Teams auf der Berechtigungsliste.
|
||||
pulls.blocked_by_rejection=Dieser Pull-Request hat Änderungen, die von einem offiziellen Reviewer angefragt wurden.
|
||||
pulls.blocked_by_official_review_requests=Dieser Pull Request hat offizielle Review-Anfragen.
|
||||
pulls.blocked_by_outdated_branch=Dieser Pull Request ist blockiert, da er veraltet ist.
|
||||
@ -1803,7 +1900,9 @@ pulls.unrelated_histories=Merge fehlgeschlagen: Der Head des Merges und die Basi
|
||||
pulls.merge_out_of_date=Merge fehlgeschlagen: Während des Mergens wurde die Basis aktualisiert. Hinweis: Versuche es erneut.
|
||||
pulls.head_out_of_date=Mergen fehlgeschlagen: Der Head wurde aktualisiert während der Merge erstellt wurde. Tipp: Versuche es erneut.
|
||||
pulls.has_merged=Fehler: Der Pull-Request wurde gemerged, du kannst den Zielbranch nicht wieder mergen oder ändern.
|
||||
pulls.push_rejected=Push fehlgeschlagen: Der Push wurde abgelehnt. Überprüfe die Git Hooks für dieses Repository.
|
||||
pulls.push_rejected_summary=Vollständige Ablehnungsmeldung
|
||||
pulls.push_rejected_no_message=Push fehlgeschlagen: Der Push wurde abgelehnt, aber es gab keine Fehlermeldung. Überprüfe die Git Hooks für dieses Repository
|
||||
pulls.open_unmerged_pull_exists=`Du kannst diesen Pull-Request nicht erneut öffnen, da noch ein anderer (#%d) mit identischen Eigenschaften offen ist.`
|
||||
pulls.status_checking=Einige Prüfungen sind noch ausstehend
|
||||
pulls.status_checks_success=Alle Prüfungen waren erfolgreich
|
||||
@ -1827,6 +1926,7 @@ pulls.cmd_instruction_checkout_title=Checkout
|
||||
pulls.cmd_instruction_checkout_desc=Wechsle auf einen neuen Branch in deinem lokalen Repository und teste die Änderungen.
|
||||
pulls.cmd_instruction_merge_title=Mergen
|
||||
pulls.cmd_instruction_merge_desc=Die Änderungen mergen und auf Gitea aktualisieren.
|
||||
pulls.cmd_instruction_merge_warning=Warnung: Dieser Vorgang kann den Pull-Request nicht mergen, da "manueller Merge" nicht aktiviert wurde
|
||||
pulls.clear_merge_message=Merge-Nachricht löschen
|
||||
pulls.clear_merge_message_hint=Das Löschen der Merge-Nachricht wird nur den Inhalt der Commit-Nachricht entfernen und generierte Git-Trailer wie "Co-Authored-By …" erhalten.
|
||||
|
||||
@ -1846,9 +1946,15 @@ pulls.delete.title=Diesen Pull-Request löschen?
|
||||
pulls.delete.text=Willst du diesen Pull-Request wirklich löschen? (Dies wird den Inhalt unwiderruflich löschen. Überlege, ob du ihn nicht lieber schließen willst, um ihn zu archivieren)
|
||||
|
||||
pulls.recently_pushed_new_branches=Du hast auf den Branch <strong>%[1]s</strong> %[2]s gepusht
|
||||
pulls.upstream_diverging_prompt_behind_1=Dieser Branch ist %[1]d Commit hinter %[2]s
|
||||
pulls.upstream_diverging_prompt_behind_n=Dieser Branch ist %[1]d Commits hinter %[2]s
|
||||
pulls.upstream_diverging_prompt_base_newer=Der Basis-Branch %s hat neue Änderungen
|
||||
pulls.upstream_diverging_merge=Fork synchronisieren
|
||||
|
||||
pull.deleted_branch=(gelöscht):%s
|
||||
pull.agit_documentation=Dokumentation zu AGit durchschauen
|
||||
|
||||
comments.edit.already_changed=Änderungen zum Kommentar konnten nicht gespeichert werden. Es scheint, dass der Inhalt bereits von einem anderen Benutzer geändert wurde. Bitte aktualisiere die Seite und bearbeite diesen erneut, um zu verhindern, dass die Änderungen des anderen Benutzers überschrieben werden
|
||||
|
||||
milestones.new=Neuer Meilenstein
|
||||
milestones.closed=Geschlossen %s
|
||||
@ -1857,6 +1963,7 @@ milestones.no_due_date=Kein Fälligkeitsdatum
|
||||
milestones.open=Öffnen
|
||||
milestones.close=Schließen
|
||||
milestones.new_subheader=Benutze Meilensteine, um Issues zu organisieren und den Fortschritt darzustellen.
|
||||
milestones.completeness=<strong>%d%%</strong> abgeschlossen
|
||||
milestones.create=Meilenstein erstellen
|
||||
milestones.title=Titel
|
||||
milestones.desc=Beschreibung
|
||||
@ -2041,12 +2148,14 @@ settings.push_mirror_sync_in_progress=Aktuell werden Änderungen auf %s gepusht.
|
||||
settings.site=Webseite
|
||||
settings.update_settings=Einstellungen speichern
|
||||
settings.update_mirror_settings=Mirror-Einstellungen aktualisieren
|
||||
settings.branches.switch_default_branch=Standardbranch wechseln
|
||||
settings.branches.update_default_branch=Standardbranch aktualisieren
|
||||
settings.branches.add_new_rule=Neue Regel hinzufügen
|
||||
settings.advanced_settings=Erweiterte Einstellungen
|
||||
settings.wiki_desc=Repository-Wiki aktivieren
|
||||
settings.use_internal_wiki=Eingebautes Wiki verwenden
|
||||
settings.default_wiki_branch_name=Standardbezeichnung für Wiki-Branch
|
||||
settings.default_wiki_everyone_access=Standard-Zugriffsberechtigung für angemeldete Benutzer:
|
||||
settings.failed_to_change_default_wiki_branch=Das Ändern des Standard-Wiki-Branches ist fehlgeschlagen.
|
||||
settings.use_external_wiki=Externes Wiki verwenden
|
||||
settings.external_wiki_url=Externe Wiki-URL
|
||||
@ -2077,6 +2186,7 @@ settings.pulls.default_delete_branch_after_merge=Standardmäßig bei Pull-Reques
|
||||
settings.pulls.default_allow_edits_from_maintainers=Änderungen von Maintainern standardmäßig erlauben
|
||||
settings.releases_desc=Repository-Releases aktivieren
|
||||
settings.packages_desc=Repository Packages Registry aktivieren
|
||||
settings.projects_desc=Projekte aktivieren
|
||||
settings.projects_mode_desc=Projekte-Modus (welche Art Projekte angezeigt werden sollen)
|
||||
settings.projects_mode_repo=Nur Repo-Projekte
|
||||
settings.projects_mode_owner=Nur Benutzer- oder Organisations-Projekte
|
||||
@ -2116,6 +2226,7 @@ settings.transfer_in_progress=Es gibt derzeit eine laufende Übertragung. Bitte
|
||||
settings.transfer_notices_1=– Du wirst keinen Zugriff mehr haben, wenn der neue Besitzer ein individueller Benutzer ist.
|
||||
settings.transfer_notices_2=– Du wirst weiterhin Zugriff haben, wenn der neue Besitzer eine Organisation ist und du einer der Besitzer bist.
|
||||
settings.transfer_notices_3=- Wenn das Repository privat ist und an einen einzelnen Benutzer übertragen wird, wird sichergestellt, dass der Benutzer mindestens Leserechte hat (und die Berechtigungen werden gegebenenfalls ändert).
|
||||
settings.transfer_notices_4=- Wenn das Repository einer Organisation gehört und du es an eine andere Organisation oder eine andere Person überträgst, verlierst du die Verlinkungen zwischen den Issues des Repositorys und dem Projektboard der Organisation.
|
||||
settings.transfer_owner=Neuer Besitzer
|
||||
settings.transfer_perform=Übertragung durchführen
|
||||
settings.transfer_started=`Für dieses Repository wurde eine Übertragung eingeleitet und wartet nun auf die Bestätigung von "%s"`
|
||||
@ -2215,6 +2326,7 @@ settings.event_wiki_desc=Wiki-Seite erstellt, umbenannt, bearbeitet oder gelösc
|
||||
settings.event_release=Release
|
||||
settings.event_release_desc=Release in einem Repository veröffentlicht, aktualisiert oder gelöscht.
|
||||
settings.event_push=Push
|
||||
settings.event_force_push=Force Push
|
||||
settings.event_push_desc=Git push in ein Repository.
|
||||
settings.event_repository=Repository
|
||||
settings.event_repository_desc=Repository erstellt oder gelöscht.
|
||||
@ -2251,6 +2363,7 @@ settings.event_pull_request_merge=Pull-Request-Merge
|
||||
settings.event_package=Paket
|
||||
settings.event_package_desc=Paket wurde in einem Repository erstellt oder gelöscht.
|
||||
settings.branch_filter=Branch-Filter
|
||||
settings.branch_filter_desc=Whitelist für Branches für Push-, Erzeugungs- und Löschevents, als Glob-Pattern beschrieben. Es werden Events für alle Branches gemeldet, falls das Pattern <code>*</code> ist, oder falls es leer ist. Siehe die <a href="%[1]s">%[2]s</a> Dokumentation für die Syntax (Englisch). Beispiele: <code>master</code>, <code>{master,release*}</code>.
|
||||
settings.authorization_header=Authorization-Header
|
||||
settings.authorization_header_desc=Wird, falls vorhanden, als Authorization-Header mitgesendet. Beispiele: %s.
|
||||
settings.active=Aktiv
|
||||
@ -2299,22 +2412,50 @@ settings.branches=Branches
|
||||
settings.protected_branch=Branch-Schutz
|
||||
settings.protected_branch.save_rule=Regel speichern
|
||||
settings.protected_branch.delete_rule=Regel löschen
|
||||
settings.protected_branch_can_push=Push erlauben?
|
||||
settings.protected_branch_can_push_yes=Du kannst pushen
|
||||
settings.protected_branch_can_push_no=Du kannst nicht pushen
|
||||
settings.branch_protection=Branch-Schutz für Branch '<b>%s</b>'
|
||||
settings.protect_this_branch=Branch-Schutz aktivieren
|
||||
settings.protect_this_branch_desc=Verhindert das Löschen und schränkt Git auf Push- und Merge-Änderungen auf dem Branch ein.
|
||||
settings.protect_disable_push=Push deaktivieren
|
||||
settings.protect_disable_push_desc=Kein Push auf diesen Branch erlauben.
|
||||
settings.protect_disable_force_push=Force-Push deaktivieren
|
||||
settings.protect_disable_force_push_desc=Force-Push auf diesen Branch nicht erlauben.
|
||||
settings.protect_enable_push=Push aktivieren
|
||||
settings.protect_enable_push_desc=Jeder, der Schreibzugriff hat, darf in diesen Branch Pushen (aber kein Force-Push).
|
||||
settings.protect_enable_force_push_all=Force-Push aktivieren
|
||||
settings.protect_enable_force_push_all_desc=Jeder mit Push-Zugriff wird in diesen Branch force-pushen können.
|
||||
settings.protect_enable_force_push_allowlist=Force-Push beschränkt auf Genehmigungsliste
|
||||
settings.protect_enable_force_push_allowlist_desc=Nur Benutzer oder Teams auf der Genehmigungsliste mit Push-Zugriff werden in diesen Branch force-pushen können.
|
||||
settings.protect_enable_merge=Merge aktivieren
|
||||
settings.protect_enable_merge_desc=Jeder mit Schreibzugriff darf die Pull-Requests in diesen Branch mergen.
|
||||
settings.protect_whitelist_committers=Genehmigungsliste für eingeschränkten Push
|
||||
settings.protect_whitelist_committers_desc=Jeder, der auf der Genehmigungsliste steht, darf in diesen Branch pushen (aber kein Force-Push).
|
||||
settings.protect_whitelist_deploy_keys=Genehmigungsliste für Deploy-Schlüssel mit Schreibzugriff zum Pushen.
|
||||
settings.protect_whitelist_users=Nutzer, die pushen dürfen:
|
||||
settings.protect_whitelist_teams=Teams, die pushen dürfen:
|
||||
settings.protect_force_push_allowlist_users=Erlaubte Benutzer für Force-Push:
|
||||
settings.protect_force_push_allowlist_teams=Erlaubte Teams für Force-Push:
|
||||
settings.protect_force_push_allowlist_deploy_keys=Genehmigungsliste für Deploy-Schlüssel mit Schreibzugriff zum Force-Push.
|
||||
settings.protect_merge_whitelist_committers=Merge-Genehmigungsliste aktivieren
|
||||
settings.protect_merge_whitelist_committers_desc=Erlaube Nutzern oder Teams auf der Genehmigungsliste Pull-Requests in diesen Branch zu mergen.
|
||||
settings.protect_merge_whitelist_users=Nutzer, die mergen dürfen:
|
||||
settings.protect_merge_whitelist_teams=Teams, die mergen dürfen:
|
||||
settings.protect_check_status_contexts=Statusprüfungen aktivieren
|
||||
settings.protect_status_check_patterns=Statuscheck-Muster:
|
||||
settings.protect_status_check_patterns_desc=Gib Muster ein, um festzulegen, welche Statusüberprüfungen durchgeführt werden müssen, bevor Branches in einen Branch, der dieser Regel entspricht, gemerged werden können. Jede Zeile gibt ein Muster an. Muster dürfen nicht leer sein.
|
||||
settings.protect_check_status_contexts_desc=Vor dem Mergen müssen Statusprüfungen bestanden werden. Wähle aus, welche Statusprüfungen erfolgreich durchgeführt werden müssen, bevor Branches in einen anderen gemergt werden können, der dieser Regel entspricht. Wenn aktiviert, müssen Commits zuerst auf einen anderen Branch gepusht werden, dann nach bestandener Statusprüfung gemergt oder direkt auf einen Branch gepusht werden, der dieser Regel entspricht. Wenn kein Kontext ausgewählt ist, muss der letzte Commit unabhängig vom Kontext erfolgreich sein.
|
||||
settings.protect_check_status_contexts_list=Statusprüfungen, die in der letzten Woche für dieses Repository gefunden wurden
|
||||
settings.protect_status_check_matched=Übereinstimmung
|
||||
settings.protect_invalid_status_check_pattern=Ungültiges Muster: "%s".
|
||||
settings.protect_no_valid_status_check_patterns=Keine gültigen Statuscheck-Muster.
|
||||
settings.protect_required_approvals=Erforderliche Zustimmungen:
|
||||
settings.protect_required_approvals_desc=Erlaube das Mergen des Pull-Requests nur mit genügend Genehmigungen.
|
||||
settings.protect_approvals_whitelist_enabled=Genehmigungen auf Benutzer oder Teams auf der Genehmigungsliste beschränken
|
||||
settings.protect_approvals_whitelist_enabled_desc=Nur Bewertungen von Benutzern auf der Genehmigungsliste oder Teams zählen zu den erforderlichen Genehmigungen. Gibt es keine Genehmigungsliste, so zählen Reviews von jedem mit Schreibzugriff zu den erforderlichen Genehmigungen.
|
||||
settings.protect_approvals_whitelist_users=Freigeschaltete Reviewer:
|
||||
settings.protect_approvals_whitelist_teams=Freigeschaltete Teams:
|
||||
settings.dismiss_stale_approvals=Entferne alte Genehmigungen
|
||||
settings.dismiss_stale_approvals_desc=Wenn neue Commits gepusht werden, die den Inhalt des Pull-Requests ändern, werden alte Genehmigungen entfernt.
|
||||
settings.ignore_stale_approvals=Veraltete Genehmigungen ignorieren
|
||||
@ -2322,12 +2463,18 @@ settings.ignore_stale_approvals_desc=Genehmigungen, die für ältere Commits ert
|
||||
settings.require_signed_commits=Signierte Commits erforderlich
|
||||
settings.require_signed_commits_desc=Pushes auf diesen Branch ablehnen, wenn Commits nicht signiert oder nicht überprüfbar sind.
|
||||
settings.protect_branch_name_pattern=Muster für geschützte Branchnamen
|
||||
settings.protect_branch_name_pattern_desc=Geschützte Branch-Namensmuster. Siehe <a href="%s">die Dokumentation</a> für die Pattern-Syntax. Beispiele: main, release/**
|
||||
settings.protect_patterns=Muster
|
||||
settings.protect_protected_file_patterns=Geschützte Dateimuster (durch Semikolon ';' getrennt):
|
||||
settings.protect_protected_file_patterns_desc=Geschützte Dateien dürfen nicht direkt geändert werden, auch wenn der Benutzer Rechte hat, Dateien in diesem Branch hinzuzufügen, zu bearbeiten oder zu löschen. Mehrere Muster können mit Semikolon (';') getrennt werden. Siehe <a href='%[1]s'>%[2]s</a> Dokumentation zur Pattern-Syntax. Beispiele: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
|
||||
settings.protect_unprotected_file_patterns=Ungeschützte Dateimuster (durch Semikolon ';' getrennt):
|
||||
settings.protect_unprotected_file_patterns_desc=Ungeschützte Dateien, die direkt geändert werden dürfen, wenn der Benutzer Schreibzugriff hat, können die Push-Beschränkung umgehen. Mehrere Muster können mit Semikolon (';') getrennt werden. Siehe <a href='%[1]s'>%[2]s</a> Dokumentation zur Mustersyntax. Beispiele: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
|
||||
settings.add_protected_branch=Schutz aktivieren
|
||||
settings.delete_protected_branch=Schutz deaktivieren
|
||||
settings.update_protect_branch_success=Branchschutzregel "%s" wurde geändert.
|
||||
settings.remove_protected_branch_success=Branchschutzregel "%s" wurde deaktiviert.
|
||||
settings.remove_protected_branch_failed=Entfernen der Branchschutzregel "%s" fehlgeschlagen.
|
||||
settings.protected_branch_deletion=Branch-Schutz deaktivieren
|
||||
settings.protected_branch_deletion_desc=Wenn du den Branch-Schutz deaktivierst, können alle Nutzer mit Schreibrechten auf den Branch pushen. Fortfahren?
|
||||
settings.block_rejected_reviews=Merge bei abgelehnten Reviews blockieren
|
||||
settings.block_rejected_reviews_desc=Mergen ist nicht möglich, wenn Änderungen durch offizielle Reviewer angefragt werden, auch wenn es genügend Zustimmungen gibt.
|
||||
@ -2335,8 +2482,11 @@ settings.block_on_official_review_requests=Mergen bei offiziellen Review-Anfrage
|
||||
settings.block_on_official_review_requests_desc=Mergen ist nicht möglich wenn offizielle Review-Anfrangen vorliegen, selbst wenn es genügend Zustimmungen gibt.
|
||||
settings.block_outdated_branch=Merge blockieren, wenn der Pull-Request veraltet ist
|
||||
settings.block_outdated_branch_desc=Mergen ist nicht möglich, wenn der Head-Branch hinter dem Basis-Branch ist.
|
||||
settings.block_admin_merge_override=Administratoren müssen die Schutzregeln für Branches befolgen
|
||||
settings.block_admin_merge_override_desc=Administratoren müssen die Schutzregeln für Branches befolgen und können sie nicht umgehen.
|
||||
settings.default_branch_desc=Wähle einen Standardbranch für Pull-Requests und Code-Commits:
|
||||
settings.merge_style_desc=Merge-Styles
|
||||
settings.default_merge_style_desc=Standard-Mergeverhalten für Pull-Requests
|
||||
settings.choose_branch=Branch wählen…
|
||||
settings.no_protected_branch=Es gibt keine geschützten Branches.
|
||||
settings.edit_protected_branch=Bearbeiten
|
||||
@ -2352,12 +2502,25 @@ settings.tags.protection.allowed.teams=Erlaubte Teams
|
||||
settings.tags.protection.allowed.noone=Niemand
|
||||
settings.tags.protection.create=Tag schützen
|
||||
settings.tags.protection.none=Es gibt keine geschützten Tags.
|
||||
settings.tags.protection.pattern.description=Du kannst einen einzigen Namen oder ein globales Schema oder einen regulären Ausdruck verwenden, um mehrere Tags zu schützen. Mehr dazu im <a target="_blank" rel="noopener" href="%s">Guide für geschützte Tags (Englisch)</a>.
|
||||
settings.bot_token=Bot-Token
|
||||
settings.chat_id=Chat-ID
|
||||
settings.thread_id=Thread-ID
|
||||
settings.matrix.homeserver_url=Homeserver-URL
|
||||
settings.matrix.room_id=Raum-ID
|
||||
settings.matrix.message_type=Nachrichtentyp
|
||||
settings.visibility.private.button=Auf privat setzen
|
||||
settings.visibility.private.text=Das Ändern der Sichtbarkeit auf privat wird das Repository nicht nur für erlaubte Mitglieder sichtbar machen, sondern kann auch die Beziehung zwischen ihm und Forks, Beobachtern und Sternen entfernen.
|
||||
settings.visibility.private.bullet_title=<strong>Das Ändern der Sichtbarkeit auf privat wird:</strong>
|
||||
settings.visibility.private.bullet_one=Das Repository nur für zugelassene Mitglieder sichtbar machen.
|
||||
settings.visibility.private.bullet_two=Kann die Beziehung zwischen ihm und <strong>Forks</strong>, <strong>Beobachtern</strong>und <strong>Sternen</strong> entfernen.
|
||||
settings.visibility.public.button=Auf öffentlich setzen
|
||||
settings.visibility.public.text=Das Ändern der Sichtbarkeit auf öffentlich macht das Repository für jeden sichtbar.
|
||||
settings.visibility.public.bullet_title=<strong>Das Ändern der Sichtbarkeit auf öffentlich wird:</strong>
|
||||
settings.visibility.public.bullet_one=Das Repository für jeden sichtbar machen.
|
||||
settings.visibility.success=Die Sichtbarkeit des Repositorys wurde geändert.
|
||||
settings.visibility.error=Beim Versuch, die Sichtbarkeit des Repositorys zu ändern, ist ein Fehler aufgetreten.
|
||||
settings.visibility.fork_error=Die Sichtbarkeit von geforkten Repositories ist nicht veränderbar.
|
||||
settings.archive.button=Repo archivieren
|
||||
settings.archive.header=Dieses Repo archivieren
|
||||
settings.archive.text=Durch das Archivieren wird ein Repo vollständig schreibgeschützt. Es wird vom Dashboard versteckt. Niemand (nicht einmal du!) wird in der Lage sein, neue Commits zu erstellen oder Issues oder Pull-Requests zu öffnen.
|
||||
@ -2469,6 +2632,7 @@ release.new_release=Neues Release
|
||||
release.draft=Entwurf
|
||||
release.prerelease=Pre-Release
|
||||
release.stable=Stabil
|
||||
release.latest=Aktuell
|
||||
release.compare=Vergleichen
|
||||
release.edit=bearbeiten
|
||||
release.ahead.commits=<strong>%d</strong> Commits
|
||||
@ -2553,6 +2717,7 @@ tag.create_success=Tag "%s" wurde erstellt.
|
||||
|
||||
topic.manage_topics=Themen verwalten
|
||||
topic.done=Fertig
|
||||
topic.count_prompt=Du kannst nicht mehr als 25 Themen auswählen
|
||||
topic.format_prompt=Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) und Punkte ('.') enthalten und bis zu 35 Zeichen lang sein. Nur Kleinbuchstaben sind zulässig.
|
||||
|
||||
find_file.go_to_file=Datei suchen
|
||||
@ -2650,6 +2815,7 @@ teams.leave.detail=%s verlassen?
|
||||
teams.can_create_org_repo=Repositories erstellen
|
||||
teams.can_create_org_repo_helper=Mitglieder können neue Repositories in der Organisation erstellen. Der Ersteller erhält Administrator-Zugriff auf das neue Repository.
|
||||
teams.none_access=Kein Zugriff
|
||||
teams.none_access_helper=Mitglieder können keine anderen Aktionen für diese Einheit anzeigen oder durchführen. Dies hat keine Wirkung auf öffentliche Repositories.
|
||||
teams.general_access=Allgemeiner Zugriff
|
||||
teams.general_access_helper=Mitgliederberechtigungen werden durch folgende Berechtigungstabelle festgelegt.
|
||||
teams.read_access=Lesen
|
||||
@ -2696,6 +2862,7 @@ teams.invite.by=Von %s eingeladen
|
||||
teams.invite.description=Bitte klicke auf die folgende Schaltfläche, um dem Team beizutreten.
|
||||
|
||||
[admin]
|
||||
maintenance=Wartung
|
||||
dashboard=Dashboard
|
||||
self_check=Selbstprüfung
|
||||
identity_access=Identität & Zugriff
|
||||
@ -2717,7 +2884,9 @@ last_page=Letzte
|
||||
total=Gesamt: %d
|
||||
settings=Administratoreinstellungen
|
||||
|
||||
dashboard.new_version_hint=Gitea %s ist jetzt verfügbar, deine derzeitige Version ist %s. Weitere Details findest du im <a target="_blank" rel="noreferrer" href="%s">Blog</a>.
|
||||
dashboard.statistic=Übersicht
|
||||
dashboard.maintenance_operations=Wartungsoperationen
|
||||
dashboard.system_status=System-Status
|
||||
dashboard.operation_name=Name der Operation
|
||||
dashboard.operation_switch=Wechseln
|
||||
@ -2758,6 +2927,7 @@ dashboard.reinit_missing_repos=Alle Git-Repositories neu einlesen, für die Eint
|
||||
dashboard.sync_external_users=Externe Benutzerdaten synchronisieren
|
||||
dashboard.cleanup_hook_task_table=Hook-Task-Tabelle bereinigen
|
||||
dashboard.cleanup_packages=Veraltete Pakete löschen
|
||||
dashboard.cleanup_actions=Abgelaufene Ressourcen von Actions bereinigen
|
||||
dashboard.server_uptime=Server-Uptime
|
||||
dashboard.current_goroutine=Aktuelle Goroutinen
|
||||
dashboard.current_memory_usage=Aktuelle Speichernutzung
|
||||
@ -2787,12 +2957,19 @@ dashboard.total_gc_time=Gesamte GC-Pause
|
||||
dashboard.total_gc_pause=Gesamte GC-Pause
|
||||
dashboard.last_gc_pause=Letzte GC-Pause
|
||||
dashboard.gc_times=Anzahl GC
|
||||
dashboard.delete_old_actions=Alle alten Aktionen aus der Datenbank löschen
|
||||
dashboard.delete_old_actions.started=Löschen aller alten Aktionen in der Datenbank gestartet.
|
||||
dashboard.update_checker=Update-Checker
|
||||
dashboard.delete_old_system_notices=Alle alten Systemmeldungen aus der Datenbank löschen
|
||||
dashboard.gc_lfs=Garbage-Collection für LFS Meta-Objekte ausführen
|
||||
dashboard.stop_zombie_tasks=Zombie-Aufgaben stoppen
|
||||
dashboard.stop_endless_tasks=Endlose Aktionen stoppen
|
||||
dashboard.cancel_abandoned_jobs=Aufgegebene Jobs abbrechen
|
||||
dashboard.start_schedule_tasks=Terminierte Aufgaben starten
|
||||
dashboard.sync_branch.started=Synchronisierung der Branches gestartet
|
||||
dashboard.sync_tag.started=Tag-Synchronisierung gestartet
|
||||
dashboard.rebuild_issue_indexer=Issue-Indexer neu bauen
|
||||
dashboard.sync_repo_licenses=Repo-Lizenzen synchronisieren
|
||||
|
||||
users.user_manage_panel=Benutzerkontenverwaltung
|
||||
users.new_account=Benutzerkonto erstellen
|
||||
@ -2864,6 +3041,10 @@ emails.not_updated=Fehler beim Aktualisieren der angeforderten E-Mail-Adresse: %
|
||||
emails.duplicate_active=Diese E-Mail-Adresse wird bereits von einem Nutzer verwendet.
|
||||
emails.change_email_header=E-Mail-Eigenschaften aktualisieren
|
||||
emails.change_email_text=Bist du dir sicher, dass du diese E-Mail-Adresse aktualisieren möchtest?
|
||||
emails.delete=E-Mail löschen
|
||||
emails.delete_desc=Willst du diese E-Mail-Adresse wirklich löschen?
|
||||
emails.deletion_success=Die E-Mail-Adresse wurde gelöscht.
|
||||
emails.delete_primary_email_error=Du kannst die primäre E-Mail-Adresse nicht löschen.
|
||||
|
||||
orgs.org_manage_panel=Organisationsverwaltung
|
||||
orgs.name=Name
|
||||
@ -2896,10 +3077,12 @@ packages.size=Größe
|
||||
packages.published=Veröffentlicht
|
||||
|
||||
defaulthooks=Standard-Webhooks
|
||||
defaulthooks.desc=Webhooks senden automatisch eine HTTP-POST-Anfrage an einen Server, wenn bestimmte Gitea-Events ausgelöst werden. Hier definierte Webhooks sind die Standardwerte, die in alle neuen Repositories kopiert werden. Mehr Infos findest du in der <a target="_blank" rel="noopener" href="%s">Webhooks-Anleitung</a> (auf Englisch).
|
||||
defaulthooks.add_webhook=Standard-Webhook hinzufügen
|
||||
defaulthooks.update_webhook=Standard-Webhook aktualisieren
|
||||
|
||||
systemhooks=System-Webhooks
|
||||
systemhooks.desc=Webhooks senden automatisch HTTP-POST-Anfragen an einen Server, wenn bestimmte Gitea-Events ausgelöst werden. Hier definierte Webhooks werden auf alle Repositories des Systems übertragen, beachte daher mögliche Performance-Einbrüche. Mehr Infos findest du in der <a target="_blank" rel="noopener" href="%s">Webhooks-Anleitung</a> (auf Englisch).
|
||||
systemhooks.add_webhook=System-Webhook hinzufügen
|
||||
systemhooks.update_webhook=System-Webhook aktualisieren
|
||||
|
||||
@ -2994,7 +3177,18 @@ auths.tips=Tipps
|
||||
auths.tips.oauth2.general=OAuth2-Authentifizierung
|
||||
auths.tips.oauth2.general.tip=Beim Registrieren einer OAuth2-Anwendung sollte die Callback-URL folgendermaßen lauten:
|
||||
auths.tip.oauth2_provider=OAuth2-Anbieter
|
||||
auths.tip.bitbucket=Registriere einen neuen OAuth-Consumer unter %s und füge die Berechtigung 'Account' - 'Read' hinzu
|
||||
auths.tip.nextcloud=Registriere über das "Settings -> Security -> OAuth 2.0 client"-Menü einen neuen "OAuth consumer" auf der Nextcloud-Instanz
|
||||
auths.tip.dropbox=Erstelle eine neue App auf %s
|
||||
auths.tip.facebook=Erstelle eine neue Anwendung auf %s und füge das Produkt "Facebook Login“ hinzu
|
||||
auths.tip.github=Erstelle unter %s eine neue OAuth-Anwendung
|
||||
auths.tip.gitlab_new=Erstelle eine neue Anwendung unter %s
|
||||
auths.tip.google_plus=Du erhältst die OAuth2-Client-Zugangsdaten in der Google-API-Konsole unter %s
|
||||
auths.tip.openid_connect=Benutze die OpenID-Connect-Discovery-URL "https://{server}/.well-known/openid-configuration", um die Endpunkte zu spezifizieren
|
||||
auths.tip.twitter=Gehe zu %s, erstelle eine Anwendung und stelle sicher, dass die Option „Allow this application to be used to Sign in with Twitter“ aktiviert ist
|
||||
auths.tip.discord=Erstelle unter %s eine neue Anwendung
|
||||
auths.tip.gitea=Registriere eine neue OAuth2-Anwendung. Eine Anleitung findest du unter %s
|
||||
auths.tip.yandex=`Erstelle eine neue Anwendung auf %s. Wähle folgende Berechtigungen aus dem "Yandex.Passport API" Bereich: "Zugriff auf E-Mail-Adresse", "Zugriff auf Benutzeravatar" und "Zugriff auf Benutzername, Vor- und Nachname, Geschlecht"`
|
||||
auths.tip.mastodon=Gebe eine benutzerdefinierte URL für die Mastodon-Instanz ein, mit der du dich authentifizieren möchtest (oder benutze die standardmäßige)
|
||||
auths.edit=Authentifikationsquelle bearbeiten
|
||||
auths.activated=Diese Authentifikationsquelle ist aktiviert
|
||||
@ -3110,6 +3304,10 @@ config.cache_adapter=Cache-Adapter
|
||||
config.cache_interval=Cache-Intervall
|
||||
config.cache_conn=Cache-Anbindung
|
||||
config.cache_item_ttl=Cache Item-TTL
|
||||
config.cache_test=Cache testen
|
||||
config.cache_test_failed=Fehler beim Prüfen des Caches: %v.
|
||||
config.cache_test_slow=Cache-Test erfolgreich, aber die Antwortzeit ist langsam: %s.
|
||||
config.cache_test_succeeded=Cache-Test erfolgreich, Antwort in %s erhalten.
|
||||
|
||||
config.session_config=Session-Konfiguration
|
||||
config.session_provider=Session-Provider
|
||||
@ -3156,6 +3354,7 @@ monitor.next=Nächste Ausführung
|
||||
monitor.previous=Letzte Ausführung
|
||||
monitor.execute_times=Ausführungen
|
||||
monitor.process=Laufende Prozesse
|
||||
monitor.stacktrace=Stacktraces
|
||||
monitor.processes_count=%d Prozesse
|
||||
monitor.download_diagnosis_report=Diagnosebericht herunterladen
|
||||
monitor.desc=Beschreibung
|
||||
@ -3163,6 +3362,8 @@ monitor.start=Startzeit
|
||||
monitor.execute_time=Ausführungszeit
|
||||
monitor.last_execution_result=Ergebnis
|
||||
monitor.process.cancel=Prozess abbrechen
|
||||
monitor.process.cancel_desc=Abbrechen eines Prozesses kann Datenverlust verursachen
|
||||
monitor.process.cancel_notices=Abbrechen: <strong>%s</strong>?
|
||||
monitor.process.children=Subprozesse
|
||||
|
||||
monitor.queues=Warteschlangen
|
||||
@ -3201,11 +3402,13 @@ notices.op=Aktion
|
||||
notices.delete_success=Diese Systemmeldung wurde gelöscht.
|
||||
|
||||
self_check.no_problem_found=Bisher wurde kein Problem festgestellt.
|
||||
self_check.startup_warnings=Warnungen beim Start:
|
||||
self_check.database_collation_mismatch=Erwarte Datenbank-Kollation: %s
|
||||
self_check.database_collation_case_insensitive=Die Datenbank verwendet die Kollation %s, was eine unsensible Kollation ist. Obwohl Gitea damit arbeiten könnte, gibt es vielleicht einige seltene Fälle, die nicht wie erwartet funktionieren.
|
||||
self_check.database_inconsistent_collation_columns=Die Datenbank verwendet die Kollation %s, aber diese Spalten verwenden unzutreffende Kollationen. Dies könnte zu unerwarteten Problemen führen.
|
||||
self_check.database_fix_mysql=Für MySQL/MariaDB-Benutzer kann man den Befehl "gitea doctor convert" oder manuell auch "ALTER ... COLLATE ..."-SQLs verwenden, um die Sortierprobleme zu beheben.
|
||||
self_check.database_fix_mssql=Für MSSQL-Benutzer kann das Problem im Moment nur durch "ALTER ... COLLATE ..." SQLs manuell behoben werden.
|
||||
self_check.location_origin_mismatch=Aktuelle URL (%[1]s) stimmt nicht mit der URL überein, die Gitea (%[2]s) sieht. Wenn du einen Reverse-Proxy verwendest, stelle bitte sicher, dass die Header "Host" und "X-Forwarded-Proto" korrekt gesetzt sind.
|
||||
|
||||
[action]
|
||||
create_repo=hat das Repository <a href="%s">%s</a> erstellt
|
||||
@ -3233,6 +3436,7 @@ mirror_sync_create=neue Referenz <a href="%[2]s">%[3]s</a> bei <a href="%[1]s">%
|
||||
mirror_sync_delete=hat die Referenz des Mirrors <code>%[2]s</code> in <a href="%[1]s">%[3]s</a> synchronisiert und gelöscht
|
||||
approve_pull_request=`hat <a href="%[1]s">%[3]s#%[2]s</a> approved`
|
||||
reject_pull_request=`schlug Änderungen für <a href="%[1]s">%[3]s#%[2]s</a> vor`
|
||||
publish_release=`veröffentlichte Release <a href="%[2]s"> "%[4]s" </a> in <a href="%[1]s">%[3]s</a>`
|
||||
review_dismissed=`verwarf das Review von <b>%[4]s</b> in <a href="%[1]s">%[3]s#%[2]s</a>`
|
||||
review_dismissed_reason=Grund:
|
||||
create_branch=legte den Branch <a href="%[2]s">%[3]s</a> in <a href="%[1]s">%[4]s</a> an
|
||||
@ -3261,6 +3465,8 @@ raw_minutes=Minuten
|
||||
|
||||
[dropzone]
|
||||
default_message=Zum Hochladen hier klicken oder Datei ablegen.
|
||||
invalid_input_type=Dateien dieses Dateityps können nicht hochgeladen werden.
|
||||
file_too_big=Dateigröße ({{filesize}} MB) überschreitet die Maximalgröße ({{maxFilesize}} MB).
|
||||
remove_file=Datei entfernen
|
||||
|
||||
[notification]
|
||||
@ -3297,6 +3503,7 @@ error.unit_not_allowed=Du hast keine Berechtigung, um auf diesen Repository-Bere
|
||||
title=Pakete
|
||||
desc=Repository-Pakete verwalten.
|
||||
empty=Noch keine Pakete vorhanden.
|
||||
no_metadata=Keine Metadaten.
|
||||
empty.documentation=Weitere Informationen zur Paket-Registry findest Du in der <a target="_blank" rel="noopener noreferrer" href="%s">Dokumentation</a>.
|
||||
empty.repo=Hast du ein Paket hochgeladen, das hier nicht angezeigt wird? Gehe zu den <a href="%[1]s">Paketeinstellungen</a> und verlinke es mit diesem Repo.
|
||||
registry.documentation=Für weitere Informationen zur %s-Registry, schaue in der <a target="_blank" rel="noopener noreferrer" href="%s">Dokumentation</a> nach.
|
||||
@ -3331,6 +3538,8 @@ alpine.repository=Repository-Informationen
|
||||
alpine.repository.branches=Branches
|
||||
alpine.repository.repositories=Repositories
|
||||
alpine.repository.architectures=Architekturen
|
||||
arch.registry=Server mit gebrauchtem Repository und Architektur zu <code>/etc/pacman.conf</code> hinzufügen:
|
||||
arch.install=Paket mit pacman synchronisieren:
|
||||
arch.repository=Repository-Informationen
|
||||
arch.repository.repositories=Repositories
|
||||
arch.repository.architectures=Architekturen
|
||||
@ -3381,6 +3590,7 @@ npm.install=Um das Paket mit npm zu installieren, führe den folgenden Befehl au
|
||||
npm.install2=oder füge es zur package.json-Datei hinzu:
|
||||
npm.dependencies=Abhängigkeiten
|
||||
npm.dependencies.development=Entwicklungsabhängigkeiten
|
||||
npm.dependencies.bundle=Gebündelte Abhängigkeiten
|
||||
npm.dependencies.peer=Peer Abhängigkeiten
|
||||
npm.dependencies.optional=Optionale Abhängigkeiten
|
||||
npm.details.tag=Tag
|
||||
@ -3512,6 +3722,7 @@ runners.status.active=Aktiv
|
||||
runners.status.offline=Offline
|
||||
runners.version=Version
|
||||
runners.reset_registration_token=Registrierungs-Token zurücksetzen
|
||||
runners.reset_registration_token_confirm=Möchtest du den aktuellen Token invalidieren und einen neuen generieren?
|
||||
runners.reset_registration_token_success=Runner-Registrierungstoken erfolgreich zurückgesetzt
|
||||
|
||||
runs.all_workflows=Alle Workflows
|
||||
@ -3521,6 +3732,7 @@ runs.pushed_by=gepusht von
|
||||
runs.invalid_workflow_helper=Die Workflow-Konfigurationsdatei ist ungültig. Bitte überprüfe Deine Konfigurationsdatei: %s
|
||||
runs.no_matching_online_runner_helper=Kein passender Runner online mit Label: %s
|
||||
runs.no_job_without_needs=Der Workflow muss mindestens einen Job ohne Abhängigkeiten enthalten.
|
||||
runs.no_job=Der Workflow muss mindestens einen Job enthalten
|
||||
runs.actor=Initiator
|
||||
runs.status=Status
|
||||
runs.actors_no_select=Alle Initiatoren
|
||||
@ -3531,12 +3743,18 @@ runs.no_workflows.quick_start=Du weißt nicht, wie du mit Gitea Actions loslegst
|
||||
runs.no_workflows.documentation=Weitere Informationen zu Gitea Actions findest du in der <a target="_blank" rel="noopener noreferrer" href="%s"> Dokumentation</a>.
|
||||
runs.no_runs=Der Workflow hat noch keine Ausführungen.
|
||||
runs.empty_commit_message=(leere Commit-Nachricht)
|
||||
runs.expire_log_message=Protokolle wurden geleert, weil sie zu alt waren.
|
||||
|
||||
workflow.disable=Workflow deaktivieren
|
||||
workflow.disable_success=Workflow '%s' erfolgreich deaktiviert.
|
||||
workflow.enable=Workflow aktivieren
|
||||
workflow.enable_success=Workflow '%s' erfolgreich aktiviert.
|
||||
workflow.disabled=Workflow ist deaktiviert.
|
||||
workflow.run=Workflow ausführen
|
||||
workflow.not_found=Workflow '%s' wurde nicht gefunden.
|
||||
workflow.run_success=Workflow '%s' erfolgreich ausgeführt.
|
||||
workflow.from_ref=Nutze Workflow von
|
||||
workflow.has_workflow_dispatch=Dieser Workflow hat einen workflow_dispatch Event-Trigger.
|
||||
|
||||
need_approval_desc=Um Workflows für den Pull-Request eines Forks auszuführen, ist eine Genehmigung erforderlich.
|
||||
|
||||
@ -3556,8 +3774,11 @@ variables.creation.success=Die Variable „%s“ wurde hinzugefügt.
|
||||
variables.update.failed=Fehler beim Bearbeiten der Variable.
|
||||
variables.update.success=Die Variable wurde bearbeitet.
|
||||
|
||||
logs.always_auto_scroll=Autoscroll für Logs immer aktivieren
|
||||
logs.always_expand_running=Laufende Logs immer erweitern
|
||||
|
||||
[projects]
|
||||
deleted.display_name=Gelöschtes Projekt
|
||||
type-1.display_name=Individuelles Projekt
|
||||
type-2.display_name=Repository-Projekt
|
||||
type-3.display_name=Organisationsprojekt
|
||||
|
@ -580,6 +580,7 @@ joined_on=Εγγράφηκε την %s
|
||||
repositories=Αποθετήρια
|
||||
activity=Δημόσια Δραστηριότητα
|
||||
followers=Ακόλουθοι
|
||||
show_more=Εμφάνιση Περισσότερων
|
||||
starred=Αγαπημένα Αποθετήρια
|
||||
watched=Ακολουθούμενα Αποθετήρια
|
||||
code=Κώδικας
|
||||
|
@ -649,6 +649,7 @@ joined_on = Joined on %s
|
||||
repositories = Repositories
|
||||
activity = Public Activity
|
||||
followers = Followers
|
||||
show_more = Show More
|
||||
starred = Starred Repositories
|
||||
watched = Watched Repositories
|
||||
code = Code
|
||||
|
@ -577,6 +577,7 @@ joined_on=Se unió el %s
|
||||
repositories=Repositorios
|
||||
activity=Actividad pública
|
||||
followers=Seguidores
|
||||
show_more=Ver más
|
||||
starred=Repositorios Favoritos
|
||||
watched=Repositorios seguidos
|
||||
code=Código
|
||||
|
@ -463,6 +463,7 @@ change_avatar=تغییر آواتار…
|
||||
repositories=مخازن
|
||||
activity=فعالیت های عمومی
|
||||
followers=دنبال کنندگان
|
||||
show_more=نمایش بیشتر
|
||||
starred=مخان ستاره دار
|
||||
watched=مخازنی که دنبال میشوند
|
||||
projects=پروژهها
|
||||
|
@ -649,6 +649,7 @@ joined_on=Inscrit le %s
|
||||
repositories=Dépôts
|
||||
activity=Activité publique
|
||||
followers=abonnés
|
||||
show_more=Voir plus
|
||||
starred=Dépôts favoris
|
||||
watched=Dépôts surveillés
|
||||
code=Code
|
||||
|
@ -649,6 +649,7 @@ joined_on=Cláraigh ar %s
|
||||
repositories=Stórais
|
||||
activity=Gníomhaíocht Phoiblí
|
||||
followers=Leantóirí
|
||||
show_more=Taispeáin Tuilleadh
|
||||
starred=Stórais Réaltaithe
|
||||
watched=Stórais Breathnaithe
|
||||
code=Cód
|
||||
@ -1945,6 +1946,8 @@ pulls.delete.title=Scrios an t-iarratas tarraingthe seo?
|
||||
pulls.delete.text=An bhfuil tú cinnte gur mhaith leat an t-iarratas tarraingthe seo a scriosadh? (Bainfidh sé seo an t-inneachar go léir go buan. Smaoinigh ar é a dhúnadh ina ionad sin, má tá sé i gceist agat é a choinneáil i gcartlann)
|
||||
|
||||
pulls.recently_pushed_new_branches=Bhrúigh tú ar bhrainse <strong>%[1]s</strong> %[2]s
|
||||
pulls.upstream_diverging_prompt_behind_1=Tá an brainse seo %[1]d tiomantas taobh thiar de %[2]s
|
||||
pulls.upstream_diverging_prompt_behind_n=Tá an brainse seo %[1]d geallta taobh thiar de %[2]s
|
||||
pulls.upstream_diverging_prompt_base_newer=Tá athruithe nua ar an mbunbhrainse %s
|
||||
pulls.upstream_diverging_merge=Forc sionc
|
||||
|
||||
@ -3719,6 +3722,7 @@ runners.status.active=Gníomhach
|
||||
runners.status.offline=As líne
|
||||
runners.version=Leagan
|
||||
runners.reset_registration_token=Athshocraigh comhartha clár
|
||||
runners.reset_registration_token_confirm=Ar mhaith leat an comhartha reatha a neamhbhailiú agus ceann nua a ghiniúint?
|
||||
runners.reset_registration_token_success=D'éirigh le hathshocrú comhartha clárúcháin an dara háit
|
||||
|
||||
runs.all_workflows=Gach Sreafaí Oibre
|
||||
@ -3770,6 +3774,8 @@ variables.creation.success=Tá an athróg "%s" curtha leis.
|
||||
variables.update.failed=Theip ar athróg a chur in eagar.
|
||||
variables.update.success=Tá an t-athróg curtha in eagar.
|
||||
|
||||
logs.always_auto_scroll=Logchomhaid scrollaithe uathoibríoch i gcónaí
|
||||
logs.always_expand_running=Leathnaigh logs reatha i gcónaí
|
||||
|
||||
[projects]
|
||||
deleted.display_name=Tionscadal scriosta
|
||||
|
@ -488,6 +488,7 @@ change_avatar=Modifica il tuo avatar…
|
||||
repositories=Repository
|
||||
activity=Attività pubblica
|
||||
followers=Seguaci
|
||||
show_more=Mostra Altro
|
||||
starred=Repositories votate
|
||||
watched=Repository Osservate
|
||||
projects=Progetti
|
||||
|
@ -649,6 +649,7 @@ joined_on=%sに登録
|
||||
repositories=リポジトリ
|
||||
activity=公開アクティビティ
|
||||
followers=フォロワー
|
||||
show_more=さらに表示
|
||||
starred=スター付きリポジトリ
|
||||
watched=ウォッチ中リポジトリ
|
||||
code=コード
|
||||
|
@ -583,6 +583,7 @@ joined_on=Pievienojās %s
|
||||
repositories=Repozitoriji
|
||||
activity=Publiskā aktivitāte
|
||||
followers=Sekotāji
|
||||
show_more=Rādīt vairāk
|
||||
starred=Atzīmēti repozitoriji
|
||||
watched=Vērotie repozitoriji
|
||||
code=Kods
|
||||
|
@ -487,6 +487,7 @@ change_avatar=Wijzig je profielfoto…
|
||||
repositories=repositories
|
||||
activity=Openbare activiteit
|
||||
followers=Volgers
|
||||
show_more=Meer weergeven
|
||||
starred=Repositories met ster
|
||||
watched=Gevolgde repositories
|
||||
projects=Projecten
|
||||
|
@ -582,6 +582,7 @@ joined_on=Inscreveu-se em %s
|
||||
repositories=Repositórios
|
||||
activity=Atividade pública
|
||||
followers=Seguidores
|
||||
show_more=Mostrar mais
|
||||
starred=Repositórios favoritos
|
||||
watched=Repositórios observados
|
||||
code=Código
|
||||
|
@ -649,6 +649,7 @@ joined_on=Inscreveu-se em %s
|
||||
repositories=Repositórios
|
||||
activity=Trabalho público
|
||||
followers=Seguidores
|
||||
show_more=Mostrar mais
|
||||
starred=Repositórios favoritos
|
||||
watched=Repositórios sob vigilância
|
||||
code=Código
|
||||
@ -1921,8 +1922,8 @@ pulls.close=Encerrar pedido de integração
|
||||
pulls.closed_at=`fechou este pedido de integração <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
pulls.reopened_at=`reabriu este pedido de integração <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
pulls.cmd_instruction_hint=`Ver <a class="show-instruction">instruções para a linha de comandos</a>.`
|
||||
pulls.cmd_instruction_checkout_title=Conferir
|
||||
pulls.cmd_instruction_checkout_desc=No seu repositório, irá criar um novo ramo para que possa testar as modificações.
|
||||
pulls.cmd_instruction_checkout_title=Checkout
|
||||
pulls.cmd_instruction_checkout_desc=A partir do seu repositório, crie um novo ramo e teste nele as modificações.
|
||||
pulls.cmd_instruction_merge_title=Integrar
|
||||
pulls.cmd_instruction_merge_desc=Integrar as modificações e enviar para o Gitea.
|
||||
pulls.cmd_instruction_merge_warning=Aviso: Esta operação não pode executar pedidos de integração porque "auto-identificar integração manual" não estava habilitado
|
||||
@ -1945,6 +1946,8 @@ pulls.delete.title=Eliminar este pedido de integração?
|
||||
pulls.delete.text=Tem a certeza que quer eliminar este pedido de integração? Isso irá remover todo o conteúdo permanentemente. Como alternativa considere fechá-lo, se pretender mantê-lo em arquivo.
|
||||
|
||||
pulls.recently_pushed_new_branches=Enviou para o ramo <strong>%[1]s</strong> %[2]s
|
||||
pulls.upstream_diverging_prompt_behind_1=Este ramo está %[1]d cometimento atrás de %[2]s
|
||||
pulls.upstream_diverging_prompt_behind_n=Este ramo está %[1]d cometimentos atrás de %[2]s
|
||||
pulls.upstream_diverging_prompt_base_newer=O ramo base %s tem novas modificações
|
||||
pulls.upstream_diverging_merge=Sincronizar derivação
|
||||
|
||||
|
@ -578,6 +578,7 @@ joined_on=Присоединил(ся/ась) %s
|
||||
repositories=Репозитории
|
||||
activity=Активность
|
||||
followers=Подписчики
|
||||
show_more=Показать больше
|
||||
starred=Избранные репозитории
|
||||
watched=Отслеживаемые репозитории
|
||||
code=Код
|
||||
|
@ -452,6 +452,7 @@ change_avatar=ඔබගේ අවතාරය වෙනස් කරන්න…
|
||||
repositories=කෝෂ්ඨ
|
||||
activity=ප්රසිද්ධ ක්රියාකාරකම
|
||||
followers=අනුගාමිකයන්
|
||||
show_more=තව පෙන්වන්න
|
||||
starred=තරු ගබඩාව
|
||||
watched=නරඹන ලද ගබඩාවලදී
|
||||
projects=ව්යාපෘති
|
||||
|
@ -631,6 +631,7 @@ joined_on=%s tarihinde katıldı
|
||||
repositories=Depolar
|
||||
activity=Genel Aktivite
|
||||
followers=Takipçiler
|
||||
show_more=Daha Fazla Göster
|
||||
starred=Yıldızlanmış depolar
|
||||
watched=İzlenen Depolar
|
||||
code=Kod
|
||||
|
@ -466,6 +466,7 @@ change_avatar=Змінити свій аватар…
|
||||
repositories=Репозиторії
|
||||
activity=Публічна активність
|
||||
followers=Читачі
|
||||
show_more=Показати більше
|
||||
starred=Обрані Репозиторії
|
||||
watched=Відстежувані репозиторії
|
||||
projects=Проєкт
|
||||
|
@ -649,6 +649,7 @@ joined_on=加入于 %s
|
||||
repositories=仓库列表
|
||||
activity=公开活动
|
||||
followers=关注者
|
||||
show_more=显示更多
|
||||
starred=已点赞
|
||||
watched=已关注仓库
|
||||
code=代码
|
||||
|
@ -647,6 +647,7 @@ joined_on=加入於 %s
|
||||
repositories=儲存庫
|
||||
activity=公開動態
|
||||
followers=追蹤者
|
||||
show_more=顯示更多
|
||||
starred=已加星號
|
||||
watched=關注的儲存庫
|
||||
code=程式碼
|
||||
|
170
package-lock.json
generated
170
package-lock.json
generated
@ -87,7 +87,7 @@
|
||||
"eslint": "8.57.0",
|
||||
"eslint-import-resolver-typescript": "3.7.0",
|
||||
"eslint-plugin-array-func": "4.0.0",
|
||||
"eslint-plugin-github": "5.1.4",
|
||||
"eslint-plugin-github": "5.0.2",
|
||||
"eslint-plugin-import-x": "4.6.1",
|
||||
"eslint-plugin-no-jquery": "3.1.0",
|
||||
"eslint-plugin-no-use-extend-native": "0.5.0",
|
||||
@ -7953,166 +7953,35 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-github": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-5.1.4.tgz",
|
||||
"integrity": "sha512-j5IgIxsDoch06zJzeqPvenfzRXDKI9Z8YwfUg1pm2ay1q44tMSFwvEu6l0uEIrTpA3v8QdPyLr98LqDl1TIhSA==",
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-github/-/eslint-plugin-github-5.0.2.tgz",
|
||||
"integrity": "sha512-nMdzWJQ5CimjQDY6SFeJ0KIXuNFf0dgDWEd4eP3UWfuTuP/dXcZJDg7MQRvAFt743T1zUi4+/HdOihfu8xJkLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint/compat": "^1.2.3",
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@github/browserslist-config": "^1.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"aria-query": "^5.3.0",
|
||||
"eslint-config-prettier": ">=8.0.0",
|
||||
"eslint-plugin-escompat": "^3.11.3",
|
||||
"eslint-plugin-escompat": "^3.3.3",
|
||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||
"eslint-plugin-filenames": "^1.3.2",
|
||||
"eslint-plugin-i18n-text": "^1.0.1",
|
||||
"eslint-plugin-import": "^2.25.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-no-only-tests": "^3.0.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-rule-documentation": ">=1.0.0",
|
||||
"globals": "^15.12.0",
|
||||
"jsx-ast-utils": "^3.3.2",
|
||||
"prettier": "^3.0.0",
|
||||
"svg-element-attributes": "^1.3.1",
|
||||
"typescript-eslint": "^8.14.0"
|
||||
"svg-element-attributes": "^1.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"eslint-ignore-errors": "bin/eslint-ignore-errors.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8 || ^9"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-github/node_modules/@eslint/compat": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.4.tgz",
|
||||
"integrity": "sha512-S8ZdQj/N69YAtuqFt7653jwcvuUj131+6qGLUyDqfDg1OIoBQ66OCuXC473YQfO2AaxITTutiRQiDwoo7ZLYyg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^9.10.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"eslint": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-github/node_modules/@eslint/eslintrc": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz",
|
||||
"integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.4",
|
||||
"debug": "^4.3.2",
|
||||
"espree": "^10.0.1",
|
||||
"globals": "^14.0.0",
|
||||
"ignore": "^5.2.0",
|
||||
"import-fresh": "^3.2.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"minimatch": "^3.1.2",
|
||||
"strip-json-comments": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-github/node_modules/@eslint/eslintrc/node_modules/globals": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
||||
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-github/node_modules/@eslint/js": {
|
||||
"version": "9.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz",
|
||||
"integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-github/node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-github/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-github/node_modules/globals": {
|
||||
"version": "15.14.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz",
|
||||
"integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-github/node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eslint-plugin-github/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"eslint": "^8.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-i18n-text": {
|
||||
@ -14063,29 +13932,6 @@
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.1.tgz",
|
||||
"integrity": "sha512-Mlaw6yxuaDEPQvb/2Qwu3/TfgeBHy9iTJ3mTwe7OvpPmF6KPQjVOfGyEJpPv6Ez2C34OODChhXrzYw/9phI0MQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.18.1",
|
||||
"@typescript-eslint/parser": "8.18.1",
|
||||
"@typescript-eslint/utils": "8.18.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typo-js": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.5.tgz",
|
||||
|
@ -86,7 +86,7 @@
|
||||
"eslint": "8.57.0",
|
||||
"eslint-import-resolver-typescript": "3.7.0",
|
||||
"eslint-plugin-array-func": "4.0.0",
|
||||
"eslint-plugin-github": "5.1.4",
|
||||
"eslint-plugin-github": "5.0.2",
|
||||
"eslint-plugin-import-x": "4.6.1",
|
||||
"eslint-plugin-no-jquery": "3.1.0",
|
||||
"eslint-plugin-no-use-extend-native": "0.5.0",
|
||||
|
@ -37,8 +37,6 @@ import (
|
||||
"code.gitea.io/gitea/routers/api/packages/vagrant"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
|
||||
@ -140,39 +138,10 @@ func CommonRoutes() *web.Router {
|
||||
}, reqPackageAccess(perm.AccessModeRead))
|
||||
r.Group("/arch", func() {
|
||||
r.Methods("HEAD,GET", "/repository.key", arch.GetRepositoryKey)
|
||||
|
||||
reqPutRepository := web.NewPathProcessor("PUT", "/<repository:*>")
|
||||
reqGetRepoArchFile := web.NewPathProcessor("HEAD,GET", "/<repository:*>/<architecture>/<filename>")
|
||||
reqDeleteRepoNameVerArch := web.NewPathProcessor("DELETE", "/<repository:*>/<name>/<version>/<architecture>")
|
||||
|
||||
r.Any("*", func(ctx *context.Context) {
|
||||
chiCtx := chi.RouteContext(ctx.Req.Context())
|
||||
path := ctx.PathParam("*")
|
||||
|
||||
if reqPutRepository.ProcessRequestPath(chiCtx, path) {
|
||||
reqPackageAccess(perm.AccessModeWrite)(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
arch.UploadPackageFile(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
if reqGetRepoArchFile.ProcessRequestPath(chiCtx, path) {
|
||||
arch.GetPackageOrRepositoryFile(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
if reqDeleteRepoNameVerArch.ProcessRequestPath(chiCtx, path) {
|
||||
reqPackageAccess(perm.AccessModeWrite)(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
arch.DeletePackageVersion(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNotFound)
|
||||
r.PathGroup("*", func(g *web.RouterPathGroup) {
|
||||
g.MatchPath("PUT", "/<repository:*>", reqPackageAccess(perm.AccessModeWrite), arch.UploadPackageFile)
|
||||
g.MatchPath("HEAD,GET", "/<repository:*>/<architecture>/<filename>", arch.GetPackageOrRepositoryFile)
|
||||
g.MatchPath("DELETE", "/<repository:*>/<name>/<version>/<architecture>", reqPackageAccess(perm.AccessModeWrite), arch.DeletePackageVersion)
|
||||
})
|
||||
}, reqPackageAccess(perm.AccessModeRead))
|
||||
r.Group("/cargo", func() {
|
||||
|
@ -335,6 +335,9 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption)
|
||||
labelIDs = append(labelIDs, int64(rv.Float()))
|
||||
case reflect.String:
|
||||
labelNames = append(labelNames, rv.String())
|
||||
default:
|
||||
ctx.Error(http.StatusBadRequest, "InvalidLabel", "a label must be an integer or a string")
|
||||
return nil, nil, fmt.Errorf("invalid label")
|
||||
}
|
||||
}
|
||||
if len(labelIDs) > 0 && len(labelNames) > 0 {
|
||||
@ -342,11 +345,20 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption)
|
||||
return nil, nil, fmt.Errorf("invalid labels")
|
||||
}
|
||||
if len(labelNames) > 0 {
|
||||
labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, labelNames)
|
||||
repoLabelIDs, err := issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, labelNames)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetLabelIDsInRepoByNames", err)
|
||||
return nil, nil, err
|
||||
}
|
||||
labelIDs = append(labelIDs, repoLabelIDs...)
|
||||
if ctx.Repo.Owner.IsOrganization() {
|
||||
orgLabelIDs, err := issues_model.GetLabelIDsInOrgByNames(ctx, ctx.Repo.Owner.ID, labelNames)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetLabelIDsInOrgByNames", err)
|
||||
return nil, nil, err
|
||||
}
|
||||
labelIDs = append(labelIDs, orgLabelIDs...)
|
||||
}
|
||||
}
|
||||
|
||||
labels, err := issues_model.GetLabelsByIDs(ctx, labelIDs, "id", "repo_id", "org_id", "name", "exclusive")
|
||||
|
@ -689,7 +689,7 @@ func Activate(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated
|
||||
user := user_model.VerifyUserActiveCode(ctx, code)
|
||||
user := user_model.VerifyUserTimeLimitCode(ctx, &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, code)
|
||||
if user == nil { // if code is wrong
|
||||
renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
|
||||
return
|
||||
@ -734,7 +734,7 @@ func ActivatePost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// TODO: ctx.Doer/ctx.Data["SignedUser"] could be nil or not the same user as the one being activated
|
||||
user := user_model.VerifyUserActiveCode(ctx, code)
|
||||
user := user_model.VerifyUserTimeLimitCode(ctx, &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, code)
|
||||
if user == nil { // if code is wrong
|
||||
renderActivationPromptMessage(ctx, ctx.Locale.Tr("auth.invalid_code"))
|
||||
return
|
||||
|
@ -113,7 +113,7 @@ func commonResetPassword(ctx *context.Context) (*user_model.User, *auth.TwoFacto
|
||||
}
|
||||
|
||||
// Fail early, don't frustrate the user
|
||||
u := user_model.VerifyUserActiveCode(ctx, code)
|
||||
u := user_model.VerifyUserTimeLimitCode(ctx, &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeResetPassword}, code)
|
||||
if u == nil {
|
||||
ctx.Flash.Error(ctx.Tr("auth.invalid_code_forgot_password", fmt.Sprintf("%s/user/forgot_password", setting.AppSubURL)), true)
|
||||
return nil, nil
|
||||
|
@ -9,6 +9,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/git"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
@ -41,16 +45,85 @@ func FetchActionTest(ctx *context.Context) {
|
||||
ctx.JSONRedirect("")
|
||||
}
|
||||
|
||||
func Tmpl(ctx *context.Context) {
|
||||
now := time.Now()
|
||||
ctx.Data["TimeNow"] = now
|
||||
ctx.Data["TimePast5s"] = now.Add(-5 * time.Second)
|
||||
ctx.Data["TimeFuture5s"] = now.Add(5 * time.Second)
|
||||
ctx.Data["TimePast2m"] = now.Add(-2 * time.Minute)
|
||||
ctx.Data["TimeFuture2m"] = now.Add(2 * time.Minute)
|
||||
ctx.Data["TimePast1y"] = now.Add(-1 * 366 * 86400 * time.Second)
|
||||
ctx.Data["TimeFuture1y"] = now.Add(1 * 366 * 86400 * time.Second)
|
||||
func prepareMockData(ctx *context.Context) {
|
||||
if ctx.Req.URL.Path == "/devtest/gitea-ui" {
|
||||
now := time.Now()
|
||||
ctx.Data["TimeNow"] = now
|
||||
ctx.Data["TimePast5s"] = now.Add(-5 * time.Second)
|
||||
ctx.Data["TimeFuture5s"] = now.Add(5 * time.Second)
|
||||
ctx.Data["TimePast2m"] = now.Add(-2 * time.Minute)
|
||||
ctx.Data["TimeFuture2m"] = now.Add(2 * time.Minute)
|
||||
ctx.Data["TimePast1y"] = now.Add(-1 * 366 * 86400 * time.Second)
|
||||
ctx.Data["TimeFuture1y"] = now.Add(1 * 366 * 86400 * time.Second)
|
||||
}
|
||||
|
||||
if ctx.Req.URL.Path == "/devtest/commit-sign-badge" {
|
||||
var commits []*asymkey.SignCommit
|
||||
mockUsers, _ := db.Find[user_model.User](ctx, user_model.SearchUserOptions{ListOptions: db.ListOptions{PageSize: 1}})
|
||||
mockUser := mockUsers[0]
|
||||
commits = append(commits, &asymkey.SignCommit{
|
||||
Verification: &asymkey.CommitVerification{},
|
||||
UserCommit: &user_model.UserCommit{
|
||||
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||
},
|
||||
})
|
||||
commits = append(commits, &asymkey.SignCommit{
|
||||
Verification: &asymkey.CommitVerification{
|
||||
Verified: true,
|
||||
Reason: "name / key-id",
|
||||
SigningUser: mockUser,
|
||||
SigningKey: &asymkey.GPGKey{KeyID: "12345678"},
|
||||
TrustStatus: "trusted",
|
||||
},
|
||||
UserCommit: &user_model.UserCommit{
|
||||
User: mockUser,
|
||||
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||
},
|
||||
})
|
||||
commits = append(commits, &asymkey.SignCommit{
|
||||
Verification: &asymkey.CommitVerification{
|
||||
Verified: true,
|
||||
Reason: "name / key-id",
|
||||
SigningUser: mockUser,
|
||||
SigningSSHKey: &asymkey.PublicKey{Fingerprint: "aa:bb:cc:dd:ee"},
|
||||
TrustStatus: "untrusted",
|
||||
},
|
||||
UserCommit: &user_model.UserCommit{
|
||||
User: mockUser,
|
||||
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||
},
|
||||
})
|
||||
commits = append(commits, &asymkey.SignCommit{
|
||||
Verification: &asymkey.CommitVerification{
|
||||
Verified: true,
|
||||
Reason: "name / key-id",
|
||||
SigningUser: mockUser,
|
||||
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()},
|
||||
},
|
||||
})
|
||||
commits = append(commits, &asymkey.SignCommit{
|
||||
Verification: &asymkey.CommitVerification{
|
||||
Warning: true,
|
||||
Reason: "gpg.error",
|
||||
SigningEmail: "test@example.com",
|
||||
},
|
||||
UserCommit: &user_model.UserCommit{
|
||||
User: mockUser,
|
||||
Commit: &git.Commit{ID: git.Sha1ObjectFormat.EmptyObjectID()},
|
||||
},
|
||||
})
|
||||
|
||||
ctx.Data["MockCommits"] = commits
|
||||
}
|
||||
}
|
||||
|
||||
func Tmpl(ctx *context.Context) {
|
||||
prepareMockData(ctx)
|
||||
if ctx.Req.Method == "POST" {
|
||||
_ = ctx.Req.ParseForm()
|
||||
ctx.Flash.Info("form: "+ctx.Req.Method+" "+ctx.Req.RequestURI+"<br>"+
|
||||
@ -60,6 +133,5 @@ func Tmpl(ctx *context.Context) {
|
||||
)
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, templates.TplName("devtest"+path.Clean("/"+ctx.PathParam("sub"))))
|
||||
}
|
||||
|
@ -162,10 +162,11 @@ func TestUpdateIssueLabel_Toggle(t *testing.T) {
|
||||
UpdateIssueLabel(ctx)
|
||||
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
|
||||
for _, issueID := range testCase.IssueIDs {
|
||||
unittest.AssertExistsIf(t, testCase.ExpectedAdd, &issues_model.IssueLabel{
|
||||
IssueID: issueID,
|
||||
LabelID: testCase.LabelID,
|
||||
})
|
||||
if testCase.ExpectedAdd {
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: testCase.LabelID})
|
||||
} else {
|
||||
unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: testCase.LabelID})
|
||||
}
|
||||
}
|
||||
unittest.CheckConsistencyFor(t, &issues_model.Label{})
|
||||
}
|
||||
|
@ -61,11 +61,20 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) {
|
||||
orgs, err := db.Find[organization.Organization](ctx, organization.FindOrgOptions{
|
||||
UserID: ctx.ContextUser.ID,
|
||||
IncludePrivate: showPrivate,
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
// query one more results (without a separate counting) to see whether we need to add the "show more orgs" link
|
||||
PageSize: setting.UI.User.OrgPagingNum + 1,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindOrgs", err)
|
||||
return
|
||||
}
|
||||
if len(orgs) > setting.UI.User.OrgPagingNum {
|
||||
orgs = orgs[:setting.UI.User.OrgPagingNum]
|
||||
ctx.Data["ShowMoreOrgs"] = true
|
||||
}
|
||||
ctx.Data["Orgs"] = orgs
|
||||
ctx.Data["HasOrgsVisible"] = organization.HasOrgsVisible(ctx, orgs, ctx.Doer)
|
||||
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"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"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@ -256,6 +257,21 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
|
||||
ctx.Data["ProfileReadme"] = profileContent
|
||||
}
|
||||
}
|
||||
case "organizations":
|
||||
orgs, count, err := db.FindAndCount[organization.Organization](ctx, organization.FindOrgOptions{
|
||||
UserID: ctx.ContextUser.ID,
|
||||
IncludePrivate: showPrivate,
|
||||
ListOptions: db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: pagingNum,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserOrganizations", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Cards"] = orgs
|
||||
total = int(count)
|
||||
default: // default to "repositories"
|
||||
repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
@ -294,31 +310,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
|
||||
}
|
||||
|
||||
pager := context.NewPagination(total, pagingNum, page, 5)
|
||||
pager.SetDefaultParams(ctx)
|
||||
pager.AddParamString("tab", tab)
|
||||
if tab != "followers" && tab != "following" && tab != "activity" && tab != "projects" {
|
||||
pager.AddParamString("language", language)
|
||||
}
|
||||
if tab == "activity" {
|
||||
if ctx.Data["Date"] != nil {
|
||||
pager.AddParamString("date", fmt.Sprint(ctx.Data["Date"]))
|
||||
}
|
||||
}
|
||||
if archived.Has() {
|
||||
pager.AddParamString("archived", fmt.Sprint(archived.Value()))
|
||||
}
|
||||
if fork.Has() {
|
||||
pager.AddParamString("fork", fmt.Sprint(fork.Value()))
|
||||
}
|
||||
if mirror.Has() {
|
||||
pager.AddParamString("mirror", fmt.Sprint(mirror.Value()))
|
||||
}
|
||||
if template.Has() {
|
||||
pager.AddParamString("template", fmt.Sprint(template.Value()))
|
||||
}
|
||||
if private.Has() {
|
||||
pager.AddParamString("private", fmt.Sprint(private.Value()))
|
||||
}
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
}
|
||||
|
||||
|
@ -93,7 +93,8 @@ func SendActivateAccountMail(locale translation.Locale, u *user_model.User) {
|
||||
// No mail service configured
|
||||
return
|
||||
}
|
||||
sendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.activate_account"), "activate account")
|
||||
opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}
|
||||
sendUserMail(locale.Language(), u, mailAuthActivate, user_model.GenerateUserTimeLimitCode(opts, u), locale.TrString("mail.activate_account"), "activate account")
|
||||
}
|
||||
|
||||
// SendResetPasswordMail sends a password reset mail to the user
|
||||
@ -103,7 +104,8 @@ func SendResetPasswordMail(u *user_model.User) {
|
||||
return
|
||||
}
|
||||
locale := translation.NewLocale(u.Language)
|
||||
sendUserMail(u.Language, u, mailAuthResetPassword, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.reset_password"), "recover account")
|
||||
opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeResetPassword}
|
||||
sendUserMail(u.Language, u, mailAuthResetPassword, user_model.GenerateUserTimeLimitCode(opts, u), locale.TrString("mail.reset_password"), "recover account")
|
||||
}
|
||||
|
||||
// SendActivateEmailMail sends confirmation email to confirm new email address
|
||||
@ -113,11 +115,12 @@ func SendActivateEmailMail(u *user_model.User, email string) {
|
||||
return
|
||||
}
|
||||
locale := translation.NewLocale(u.Language)
|
||||
opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateEmail, NewEmail: email}
|
||||
data := map[string]any{
|
||||
"locale": locale,
|
||||
"DisplayName": u.DisplayName(),
|
||||
"ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale),
|
||||
"Code": u.GenerateEmailActivateCode(email),
|
||||
"Code": user_model.GenerateUserTimeLimitCode(opts, u),
|
||||
"Email": email,
|
||||
"Language": locale.Language(),
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ func TestRemoveOrgUser(t *testing.T) {
|
||||
|
||||
testSuccess := func(org *organization.Organization, user *user_model.User) {
|
||||
expectedNumMembers := org.NumMembers
|
||||
if unittest.BeanExists(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID}) {
|
||||
if unittest.GetBean(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID}) != nil {
|
||||
expectedNumMembers--
|
||||
}
|
||||
assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user))
|
||||
|
@ -64,7 +64,8 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
|
||||
}
|
||||
|
||||
// user should be a collaborator or a member of the organization for base repo
|
||||
if !issue.Poster.IsAdmin {
|
||||
canCreate := issue.Poster.IsAdmin || pr.Flow == issues_model.PullRequestFlowAGit
|
||||
if !canCreate {
|
||||
canCreate, err := repo_model.IsOwnerMemberCollaborator(ctx, repo, issue.Poster.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -1,7 +1,11 @@
|
||||
{{.Message}}
|
||||
{{if .Details}}
|
||||
<details>
|
||||
<summary>{{.Summary}}</summary>
|
||||
<code>
|
||||
{{.Details | SanitizeHTML}}
|
||||
</code>
|
||||
<code>{{.Details | SanitizeHTML}}</code>
|
||||
</details>
|
||||
{{else}}
|
||||
<div>
|
||||
{{.Summary}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
13
templates/devtest/commit-sign-badge.tmpl
Normal file
13
templates/devtest/commit-sign-badge.tmpl
Normal file
@ -0,0 +1,13 @@
|
||||
{{template "devtest/devtest-header"}}
|
||||
<div class="page-content devtest ui container">
|
||||
<div>
|
||||
<h1>Commit Sign Badges</h1>
|
||||
{{range $commit := .MockCommits}}
|
||||
<div class="flex-text-block tw-my-2">
|
||||
{{template "repo/commit_sign_badge" dict "Commit" $commit "CommitBaseLink" "/devtest/commit" "CommitSignVerification" $commit.Verification}}
|
||||
{{template "repo/commit_sign_badge" dict "CommitSignVerification" $commit.Verification}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{template "devtest/devtest-footer"}}
|
@ -29,7 +29,8 @@
|
||||
</div>
|
||||
<p class="info tw-flex tw-items-center tw-my-1">{{svg "octicon-git-commit" 16 "tw-mr-1"}}<a href="{{.RepoLink}}/commit/{{PathEscape .DefaultBranchBranch.DBBranch.CommitID}}">{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}}</a> · <span class="commit-message">{{ctx.RenderUtils.RenderCommitMessage .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeMetas ctx)}}</span> · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DefaultBranchBranch.DBBranch.CommitTime}}{{if .DefaultBranchBranch.DBBranch.Pusher}} {{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}</p>
|
||||
</td>
|
||||
<td class="right aligned middle aligned overflow-visible">
|
||||
{{/* FIXME: here and below, the tw-overflow-visible is not quite right but it is still needed the moment: to show the important buttons when the width is narrow */}}
|
||||
<td class="right aligned middle aligned tw-overflow-visible">
|
||||
{{if and $.IsWriter (not $.Repository.IsArchived) (not .IsDeleted)}}
|
||||
<button class="btn interact-bg show-create-branch-modal tw-p-2"
|
||||
data-modal="#create-branch-modal"
|
||||
@ -148,7 +149,8 @@
|
||||
{{end}}
|
||||
{{end}}
|
||||
</td>
|
||||
<td class="three wide right aligned overflow-visible">
|
||||
{{/* FIXME: here and above, the tw-overflow-visible is not quite right */}}
|
||||
<td class="three wide right aligned tw-overflow-visible">
|
||||
{{if and $.IsWriter (not $.Repository.IsArchived) (not .DBBranch.IsDeleted)}}
|
||||
<button class="btn interact-bg tw-p-2 show-modal show-create-branch-modal"
|
||||
data-branch-from="{{.DBBranch.Name}}"
|
||||
|
@ -1,23 +1,9 @@
|
||||
{{template "base/head" .}}
|
||||
{{$commitLinkBase := print $.RepoLink (Iif $.PageIsWiki "/wiki" "") "/commit"}}
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository diff">
|
||||
{{template "repo/header" .}}
|
||||
<div class="ui container fluid padded">
|
||||
{{$class := ""}}
|
||||
{{if .Commit.Signature}}
|
||||
{{$class = (print $class " isSigned")}}
|
||||
{{if .Verification.Verified}}
|
||||
{{if eq .Verification.TrustStatus "trusted"}}
|
||||
{{$class = (print $class " isVerified")}}
|
||||
{{else if eq .Verification.TrustStatus "untrusted"}}
|
||||
{{$class = (print $class " isVerifiedUntrusted")}}
|
||||
{{else}}
|
||||
{{$class = (print $class " isVerifiedUnmatched")}}
|
||||
{{end}}
|
||||
{{else if .Verification.Warning}}
|
||||
{{$class = (print $class " isWarning")}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
<div class="ui top attached header clearing segment tw-relative commit-header {{$class}}">
|
||||
<div class="ui top attached header clearing segment tw-relative commit-header">
|
||||
<div class="tw-flex tw-mb-4 tw-gap-1">
|
||||
<h3 class="tw-mb-0 tw-flex-1"><span class="commit-summary" title="{{.Commit.Summary}}">{{ctx.RenderUtils.RenderCommitMessage .Commit.Message ($.Repository.ComposeMetas ctx)}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3>
|
||||
{{if not $.PageIsWiki}}
|
||||
@ -142,125 +128,59 @@
|
||||
{{end}}
|
||||
{{template "repo/commit_load_branches_and_tags" .}}
|
||||
</div>
|
||||
<div class="ui{{if not .Commit.Signature}} bottom{{end}} attached segment tw-flex tw-items-center tw-justify-between tw-py-1 commit-header-row tw-flex-wrap {{$class}}">
|
||||
<div class="tw-flex tw-items-center author">
|
||||
{{if .Author}}
|
||||
{{ctx.AvatarUtils.Avatar .Author 28 "tw-mr-2"}}
|
||||
{{if .Author.FullName}}
|
||||
<a href="{{.Author.HomeLink}}"><strong>{{.Author.FullName}}</strong></a>
|
||||
{{else}}
|
||||
<a href="{{.Author.HomeLink}}"><strong>{{.Commit.Author.Name}}</strong></a>
|
||||
{{end}}
|
||||
|
||||
<div class="ui bottom attached segment flex-text-block tw-flex-wrap">
|
||||
<div class="flex-text-inline">
|
||||
{{if .Author}}
|
||||
{{ctx.AvatarUtils.Avatar .Author 20}}
|
||||
{{if .Author.FullName}}
|
||||
<a href="{{.Author.HomeLink}}"><strong>{{.Author.FullName}}</strong></a>
|
||||
{{else}}
|
||||
{{ctx.AvatarUtils.AvatarByEmail .Commit.Author.Email .Commit.Author.Email 28 "tw-mr-2"}}
|
||||
<strong>{{.Commit.Author.Name}}</strong>
|
||||
<a href="{{.Author.HomeLink}}"><strong>{{.Commit.Author.Name}}</strong></a>
|
||||
{{end}}
|
||||
<span class="text grey tw-ml-2" id="authored-time">{{DateUtils.TimeSince .Commit.Author.When}}</span>
|
||||
{{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}}
|
||||
<span class="text grey tw-mx-2">{{ctx.Locale.Tr "repo.diff.committed_by"}}</span>
|
||||
{{if ne .Verification.CommittingUser.ID 0}}
|
||||
{{ctx.AvatarUtils.Avatar .Verification.CommittingUser 28 "tw-mx-2"}}
|
||||
<a href="{{.Verification.CommittingUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong></a>
|
||||
{{else}}
|
||||
{{ctx.AvatarUtils.AvatarByEmail .Commit.Committer.Email .Commit.Committer.Name 28 "tw-mr-2"}}
|
||||
<strong>{{.Commit.Committer.Name}}</strong>
|
||||
{{else}}
|
||||
{{ctx.AvatarUtils.AvatarByEmail .Commit.Author.Email .Commit.Author.Email 20}}
|
||||
<strong>{{.Commit.Author.Name}}</strong>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<span class="text grey">{{DateUtils.TimeSince .Commit.Author.When}}</span>
|
||||
|
||||
<div class="flex-text-inline">
|
||||
{{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}}
|
||||
<span class="text grey">{{ctx.Locale.Tr "repo.diff.committed_by"}}</span>
|
||||
{{if ne .Verification.CommittingUser.ID 0}}
|
||||
{{ctx.AvatarUtils.Avatar .Verification.CommittingUser 20}}
|
||||
<a href="{{.Verification.CommittingUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong></a>
|
||||
{{else}}
|
||||
{{ctx.AvatarUtils.AvatarByEmail .Commit.Committer.Email .Commit.Committer.Name 20}}
|
||||
<strong>{{.Commit.Committer.Name}}</strong>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if .Verification}}
|
||||
{{template "repo/commit_sign_badge" dict "CommitSignVerification" .Verification}}
|
||||
{{end}}
|
||||
|
||||
<div class="tw-flex-1"></div>
|
||||
|
||||
<div class="flex-text-inline tw-gap-5">
|
||||
{{if .Parents}}
|
||||
<div class="flex-text-inline">
|
||||
<span>{{ctx.Locale.Tr "repo.diff.parent"}}</span>
|
||||
{{range .Parents}}
|
||||
<a class="ui label commit-id-short" href="{{$commitLinkBase}}/{{PathEscape .}}">{{ShortSha .}}</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="tw-flex tw-items-center">
|
||||
{{if .Parents}}
|
||||
<div>
|
||||
<span>{{ctx.Locale.Tr "repo.diff.parent"}}</span>
|
||||
{{range .Parents}}
|
||||
{{if $.PageIsWiki}}
|
||||
<a class="ui primary sha label" href="{{$.RepoLink}}/wiki/commit/{{PathEscape .}}">{{ShortSha .}}</a>
|
||||
{{else}}
|
||||
<a class="ui primary sha label" href="{{$.RepoLink}}/commit/{{PathEscape .}}">{{ShortSha .}}</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="item">
|
||||
<span>{{ctx.Locale.Tr "repo.diff.commit"}}</span>
|
||||
<span class="ui primary sha label">{{ShortSha .CommitID}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{if .Commit.Signature}}
|
||||
<div class="ui bottom attached message tw-text-left tw-flex tw-items-center tw-justify-between commit-header-row tw-flex-wrap tw-mb-0 {{$class}}">
|
||||
<div class="tw-flex tw-items-center">
|
||||
{{if .Verification.Verified}}
|
||||
{{if ne .Verification.SigningUser.ID 0}}
|
||||
{{svg "gitea-lock" 16 "tw-mr-2"}}
|
||||
{{if eq .Verification.TrustStatus "trusted"}}
|
||||
<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by"}}:</span>
|
||||
{{else if eq .Verification.TrustStatus "untrusted"}}
|
||||
<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user"}}:</span>
|
||||
{{else}}
|
||||
<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}:</span>
|
||||
{{end}}
|
||||
{{ctx.AvatarUtils.Avatar .Verification.SigningUser 28 "tw-mr-2"}}
|
||||
<a href="{{.Verification.SigningUser.HomeLink}}"><strong>{{.Verification.SigningUser.GetDisplayName}}</strong></a>
|
||||
{{else}}
|
||||
<span title="{{ctx.Locale.Tr "gpg.default_key"}}">{{svg "gitea-lock-cog" 16 "tw-mr-2"}}</span>
|
||||
<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.signed_by"}}:</span>
|
||||
{{ctx.AvatarUtils.AvatarByEmail .Verification.SigningEmail "" 28 "tw-mr-2"}}
|
||||
<strong>{{.Verification.SigningUser.GetDisplayName}}</strong>
|
||||
{{end}}
|
||||
{{else}}
|
||||
{{svg "gitea-unlock" 16 "tw-mr-2"}}
|
||||
<span class="ui text">{{ctx.Locale.Tr .Verification.Reason}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="tw-flex tw-items-center">
|
||||
{{if .Verification.Verified}}
|
||||
{{if ne .Verification.SigningUser.ID 0}}
|
||||
{{svg "octicon-verified" 16 "tw-mr-2"}}
|
||||
{{if .Verification.SigningSSHKey}}
|
||||
<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
|
||||
{{.Verification.SigningSSHKey.Fingerprint}}
|
||||
{{else}}
|
||||
<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
|
||||
{{.Verification.SigningKey.PaddedKeyID}}
|
||||
{{end}}
|
||||
{{else}}
|
||||
{{svg "octicon-unverified" 16 "tw-mr-2"}}
|
||||
{{if .Verification.SigningSSHKey}}
|
||||
<span class="ui text tw-mr-2" data-tooltip-content="{{ctx.Locale.Tr "gpg.default_key"}}">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
|
||||
{{.Verification.SigningSSHKey.Fingerprint}}
|
||||
{{else}}
|
||||
<span class="ui text tw-mr-2" data-tooltip-content="{{ctx.Locale.Tr "gpg.default_key"}}">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
|
||||
{{.Verification.SigningKey.PaddedKeyID}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{else if .Verification.Warning}}
|
||||
{{svg "octicon-unverified" 16 "tw-mr-2"}}
|
||||
{{if .Verification.SigningSSHKey}}
|
||||
<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
|
||||
{{.Verification.SigningSSHKey.Fingerprint}}
|
||||
{{else}}
|
||||
<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
|
||||
{{.Verification.SigningKey.PaddedKeyID}}
|
||||
{{end}}
|
||||
{{else}}
|
||||
{{if .Verification.SigningKey}}
|
||||
{{if ne .Verification.SigningKey.KeyID ""}}
|
||||
{{svg "octicon-verified" 16 "tw-mr-2"}}
|
||||
<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.gpg_key_id"}}:</span>
|
||||
{{.Verification.SigningKey.PaddedKeyID}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if .Verification.SigningSSHKey}}
|
||||
{{if ne .Verification.SigningSSHKey.Fingerprint ""}}
|
||||
{{svg "octicon-verified" 16 "tw-mr-2"}}
|
||||
<span class="ui text tw-mr-2">{{ctx.Locale.Tr "repo.commits.ssh_key_fingerprint"}}:</span>
|
||||
{{.Verification.SigningSSHKey.Fingerprint}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
<div class="flex-text-inline">
|
||||
<span>{{ctx.Locale.Tr "repo.diff.commit"}}</span>
|
||||
<a class="ui label commit-id-short" href="{{$commitLinkBase}}/{{PathEscape .CommitID}}">{{ShortSha .CommitID}}</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if .NoteRendered}}
|
||||
<div class="ui top attached header segment git-notes">
|
||||
{{svg "octicon-note" 16 "tw-mr-2"}}
|
||||
@ -276,12 +196,13 @@
|
||||
{{else}}
|
||||
<strong>{{.NoteCommit.Author.Name}}</strong>
|
||||
{{end}}
|
||||
<span class="text grey" id="note-authored-time">{{DateUtils.TimeSince .NoteCommit.Author.When}}</span>
|
||||
<span class="text grey">{{DateUtils.TimeSince .NoteCommit.Author.When}}</span>
|
||||
</div>
|
||||
<div class="ui bottom attached info segment git-notes">
|
||||
<pre class="commit-body">{{.NoteRendered | SanitizeHTML}}</pre>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{template "repo/diff/box" .}}
|
||||
</div>
|
||||
</div>
|
||||
|
78
templates/repo/commit_sign_badge.tmpl
Normal file
78
templates/repo/commit_sign_badge.tmpl
Normal file
@ -0,0 +1,78 @@
|
||||
{{/* Template attributes:
|
||||
* Commit
|
||||
* CommitBaseLink
|
||||
* CommitSignVerification
|
||||
If you'd like to modify this template, you could test it on the devtest page.
|
||||
ATTENTION: this template could be re-rendered many times (on the graph and commit list page),
|
||||
so this template should be kept as small as possbile, DO NOT put large components like modal/dialog into it.
|
||||
*/}}
|
||||
{{- $commit := $.Commit -}}
|
||||
{{- $commitBaseLink := $.CommitBaseLink -}}
|
||||
{{- $verification := $.CommitSignVerification -}}
|
||||
|
||||
{{- $extraClass := "" -}}
|
||||
{{- $verified := false -}}
|
||||
{{- $signingUser := NIL -}}
|
||||
{{- $signingEmail := "" -}}
|
||||
{{- $msgReasonPrefix := "" -}}
|
||||
{{- $msgReason := "" -}}
|
||||
{{- $msgSigningKey := "" -}}
|
||||
|
||||
{{- if $verification -}}
|
||||
{{- $signingUser = $verification.SigningUser -}}
|
||||
{{- $signingEmail = $verification.SigningEmail -}}
|
||||
{{- $extraClass = print $extraClass " commit-is-signed" -}}
|
||||
{{- if $verification.Verified -}}
|
||||
{{- /* reason is "{name} / {key-id}" */ -}}
|
||||
{{- $msgReason = $verification.Reason -}}
|
||||
{{- $verified = true -}}
|
||||
{{- if eq $verification.TrustStatus "trusted" -}}
|
||||
{{- $extraClass = print $extraClass " sign-trusted" -}}
|
||||
{{- else if eq $verification.TrustStatus "untrusted" -}}
|
||||
{{- $extraClass = print $extraClass " sign-untrusted" -}}
|
||||
{{- $msgReasonPrefix = ctx.Locale.Tr "repo.commits.signed_by_untrusted_user" -}}
|
||||
{{- else -}}
|
||||
{{- $extraClass = print $extraClass " sign-unmatched" -}}
|
||||
{{- $msgReasonPrefix = ctx.Locale.Tr "repo.commits.signed_by_untrusted_user_unmatched" -}}
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
{{- if $verification.Warning -}}
|
||||
{{- $extraClass = print $extraClass " sign-warning" -}}
|
||||
{{- end -}}
|
||||
{{- $msgReason = ctx.Locale.Tr $verification.Reason -}}{{- /* dirty part: it is the translation key ..... */ -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- if $msgReasonPrefix -}}
|
||||
{{- $msgReason = print $msgReasonPrefix ": " $msgReason -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- if $verification.SigningSSHKey -}}
|
||||
{{- $msgSigningKey = print (ctx.Locale.Tr "repo.commits.ssh_key_fingerprint") ": " $verification.SigningSSHKey.Fingerprint -}}
|
||||
{{- else if $verification.SigningKey -}}
|
||||
{{- $msgSigningKey = print (ctx.Locale.Tr "repo.commits.gpg_key_id") ": " $verification.SigningKey.PaddedKeyID -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- if $commit -}}
|
||||
<a {{if $commitBaseLink}}href="{{$commitBaseLink}}/{{$commit.ID}}"{{end}} class="ui label commit-id-short {{$extraClass}}" rel="nofollow">
|
||||
{{- ShortSha $commit.ID.String -}}
|
||||
{{- end -}}
|
||||
<span class="ui label commit-sign-badge {{$extraClass}}">
|
||||
{{- if $verified -}}
|
||||
{{- if and $signingUser $signingUser.ID -}}
|
||||
<span data-tooltip-content="{{$msgReason}}">{{svg "gitea-lock"}}</span>
|
||||
<span data-tooltip-content="{{$msgSigningKey}}">{{ctx.AvatarUtils.Avatar $signingUser 16}}</span>
|
||||
{{- else -}}
|
||||
<span data-tooltip-content="{{$msgReason}}">{{svg "gitea-lock-cog"}}</span>
|
||||
<span data-tooltip-content="{{$msgSigningKey}}">{{ctx.AvatarUtils.AvatarByEmail $signingEmail "" 16}}</span>
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
<span data-tooltip-content="{{$msgReason}}">{{svg "gitea-unlock"}}</span>
|
||||
{{- end -}}
|
||||
</span>
|
||||
|
||||
{{- if $commit -}}
|
||||
</a>
|
||||
{{- end -}}
|
||||
|
||||
{{- /* This template should be kept as small as possbile, DO NOT put large components like modal/dialog into it. */ -}}
|
@ -1,10 +1,10 @@
|
||||
{{if .Statuses}}
|
||||
{{if and (eq (len .Statuses) 1) .Status.TargetURL}}
|
||||
<a class="tw-align-middle {{.AdditionalClasses}} tw-no-underline" data-tippy="commit-statuses" href="{{.Status.TargetURL}}">
|
||||
<a class="flex-text-inline tw-no-underline {{.AdditionalClasses}}" data-tippy="commit-statuses" href="{{.Status.TargetURL}}">
|
||||
{{template "repo/commit_status" .Status}}
|
||||
</a>
|
||||
{{else}}
|
||||
<span class="tw-align-middle {{.AdditionalClasses}}" data-tippy="commit-statuses" tabindex="0">
|
||||
<span class="flex-text-inline {{.AdditionalClasses}}" data-tippy="commit-statuses" tabindex="0">
|
||||
{{template "repo/commit_status" .Status}}
|
||||
</span>
|
||||
{{end}}
|
||||
|
@ -28,33 +28,15 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="sha">
|
||||
{{$class := "ui sha label"}}
|
||||
{{if .Signature}}
|
||||
{{$class = (print $class " isSigned")}}
|
||||
{{if .Verification.Verified}}
|
||||
{{if eq .Verification.TrustStatus "trusted"}}
|
||||
{{$class = (print $class " isVerified")}}
|
||||
{{else if eq .Verification.TrustStatus "untrusted"}}
|
||||
{{$class = (print $class " isVerifiedUntrusted")}}
|
||||
{{else}}
|
||||
{{$class = (print $class " isVerifiedUnmatched")}}
|
||||
{{end}}
|
||||
{{else if .Verification.Warning}}
|
||||
{{$class = (print $class " isWarning")}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{$commitShaLink := ""}}
|
||||
{{$commitBaseLink := ""}}
|
||||
{{if $.PageIsWiki}}
|
||||
{{$commitShaLink = (printf "%s/wiki/commit/%s" $commitRepoLink (PathEscape .ID.String))}}
|
||||
{{$commitBaseLink = printf "%s/wiki/commit" $commitRepoLink}}
|
||||
{{else if $.PageIsPullCommits}}
|
||||
{{$commitShaLink = (printf "%s/pulls/%d/commits/%s" $commitRepoLink $.Issue.Index (PathEscape .ID.String))}}
|
||||
{{$commitBaseLink = printf "%s/pulls/%d/commits" $commitRepoLink $.Issue.Index}}
|
||||
{{else if $.Reponame}}
|
||||
{{$commitShaLink = (printf "%s/commit/%s" $commitRepoLink (PathEscape .ID.String))}}
|
||||
{{$commitBaseLink = printf "%s/commit" $commitRepoLink}}
|
||||
{{end}}
|
||||
<a {{if $commitShaLink}}href="{{$commitShaLink}}"{{end}} class="{{$class}}">
|
||||
<span class="shortsha">{{ShortSha .ID.String}}</span>
|
||||
{{if .Signature}}{{template "repo/shabox_badge" dict "root" $ "verification" .Verification}}{{end}}
|
||||
</a>
|
||||
{{template "repo/commit_sign_badge" dict "Commit" . "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}}
|
||||
</td>
|
||||
<td class="message">
|
||||
<span class="message-wrapper">
|
||||
|
@ -3,7 +3,7 @@
|
||||
{{range .comment.Commits}}
|
||||
{{$tag := printf "%s-%d" $.comment.HashTag $index}}
|
||||
{{$index = Eval $index "+" 1}}
|
||||
<div class="singular-commit" id="{{$tag}}">
|
||||
<div class="flex-text-block" id="{{$tag}}">{{/*singular-commit*/}}
|
||||
<span class="badge badge-commit">{{svg "octicon-git-commit"}}</span>
|
||||
{{if .User}}
|
||||
<a class="avatar" href="{{.User.HomeLink}}">{{ctx.AvatarUtils.Avatar .User 20}}</a>
|
||||
@ -11,7 +11,8 @@
|
||||
{{ctx.AvatarUtils.AvatarByEmail .Author.Email .Author.Name 20}}
|
||||
{{end}}
|
||||
|
||||
{{$commitLink:= printf "%s/commit/%s" $.comment.Issue.PullRequest.BaseRepo.Link (PathEscape .ID.String)}}
|
||||
{{$commitBaseLink := printf "%s/commit" $.comment.Issue.PullRequest.BaseRepo.Link}}
|
||||
{{$commitLink:= printf "%s/%s" $commitBaseLink (PathEscape .ID.String)}}
|
||||
|
||||
<span class="tw-flex-1 tw-font-mono gt-ellipsis" title="{{.Summary}}">
|
||||
{{- ctx.RenderUtils.RenderCommitMessageLinkSubject .Message $commitLink ($.comment.Issue.PullRequest.BaseRepo.ComposeMetas ctx) -}}
|
||||
@ -21,29 +22,9 @@
|
||||
<button class="ui button ellipsis-button show-panel toggle" data-panel="[data-singular-commit-body-for='{{$tag}}']">...</button>
|
||||
{{end}}
|
||||
|
||||
<span class="shabox tw-flex tw-items-center">
|
||||
<span class="tw-flex tw-items-center tw-gap-2">
|
||||
{{template "repo/commit_statuses" dict "Status" .Status "Statuses" .Statuses}}
|
||||
{{$class := "ui sha label"}}
|
||||
{{if .Signature}}
|
||||
{{$class = (print $class " isSigned")}}
|
||||
{{if .Verification.Verified}}
|
||||
{{if eq .Verification.TrustStatus "trusted"}}
|
||||
{{$class = (print $class " isVerified")}}
|
||||
{{else if eq .Verification.TrustStatus "untrusted"}}
|
||||
{{$class = (print $class " isVerifiedUntrusted")}}
|
||||
{{else}}
|
||||
{{$class = (print $class " isVerifiedUnmatched")}}
|
||||
{{end}}
|
||||
{{else if .Verification.Warning}}
|
||||
{{$class = (print $class " isWarning")}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
<a href="{{$commitLink}}" rel="nofollow" class="tw-ml-2 {{$class}}">
|
||||
<span class="shortsha">{{ShortSha .ID.String}}</span>
|
||||
{{if .Signature}}
|
||||
{{template "repo/shabox_badge" dict "root" $.root "verification" .Verification}}
|
||||
{{end}}
|
||||
</a>
|
||||
{{template "repo/commit_sign_badge" dict "Commit" . "CommitBaseLink" $commitBaseLink "CommitSignVerification" .Verification}}
|
||||
</span>
|
||||
</div>
|
||||
{{if IsMultilineCommitMessage .Message}}
|
||||
|
@ -235,7 +235,7 @@
|
||||
|
||||
{{if and (not $.Repository.IsArchived) (not .DiffNotAvailable)}}
|
||||
<template id="issue-comment-editor-template">
|
||||
<div class="ui form comment">
|
||||
<form class="ui form comment">
|
||||
{{template "shared/combomarkdowneditor" (dict
|
||||
"CustomInit" true
|
||||
"MarkdownPreviewInRepo" $.Repository
|
||||
@ -252,7 +252,7 @@
|
||||
<button class="ui cancel button">{{ctx.Locale.Tr "repo.issues.cancel"}}</button>
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "repo.issues.save"}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
{{end}}
|
||||
{{if (not .DiffNotAvailable)}}
|
||||
|
@ -1,5 +1,7 @@
|
||||
{{$file := .file}}
|
||||
{{$blobExcerptLink := print (or ctx.RootData.CommitRepoLink ctx.RootData.RepoLink) (Iif $.root.PageIsWiki "/wiki" "") "/blob_excerpt/" (PathEscape $.root.AfterCommitID) "?"}}
|
||||
{{$repoLink := or ctx.RootData.CommitRepoLink ctx.RootData.RepoLink}}
|
||||
{{$afterCommitID := or $.root.AfterCommitID "no-after-commit-id"}}{{/* this tmpl is also used by the PR Conversation page, so the "AfterCommitID" may not exist */}}
|
||||
{{$blobExcerptLink := print $repoLink (Iif $.root.PageIsWiki "/wiki" "") "/blob_excerpt/" (PathEscape $afterCommitID) "?"}}
|
||||
<colgroup>
|
||||
<col width="50">
|
||||
<col width="50">
|
||||
|
@ -5,33 +5,13 @@
|
||||
{{if $commit.OnlyRelation}}
|
||||
<span></span>
|
||||
{{else}}
|
||||
<span class="sha" id="{{$commit.ShortRev}}">
|
||||
{{$class := "ui sha label"}}
|
||||
{{if $commit.Commit.Signature}}
|
||||
{{$class = (print $class " isSigned")}}
|
||||
{{if $commit.Verification.Verified}}
|
||||
{{if eq $commit.Verification.TrustStatus "trusted"}}
|
||||
{{$class = (print $class " isVerified")}}
|
||||
{{else if eq $commit.Verification.TrustStatus "untrusted"}}
|
||||
{{$class = (print $class " isVerifiedUntrusted")}}
|
||||
{{else}}
|
||||
{{$class = (print $class " isVerifiedUnmatched")}}
|
||||
{{end}}
|
||||
{{else if $commit.Verification.Warning}}
|
||||
{{$class = (print $class " isWarning")}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
<a href="{{$.RepoLink}}/commit/{{$commit.Rev|PathEscape}}" rel="nofollow" class="{{$class}}">
|
||||
<span class="shortsha">{{ShortSha $commit.Commit.ID.String}}</span>
|
||||
{{- if $commit.Commit.Signature -}}
|
||||
{{template "repo/shabox_badge" dict "root" $ "verification" $commit.Verification}}
|
||||
{{- end -}}
|
||||
</a>
|
||||
</span>
|
||||
<span class="message tw-inline-block gt-ellipsis tw-mr-2">
|
||||
{{template "repo/commit_sign_badge" dict "Commit" $commit.Commit "CommitBaseLink" (print $.RepoLink "/commit") "CommitSignVerification" $commit.Verification}}
|
||||
|
||||
<span class="message tw-inline-block gt-ellipsis">
|
||||
<span>{{ctx.RenderUtils.RenderCommitMessage $commit.Subject ($.Repository.ComposeMetas ctx)}}</span>
|
||||
</span>
|
||||
<span class="commit-refs tw-flex tw-items-center tw-mr-1">
|
||||
|
||||
<span class="commit-refs flex-text-inline">
|
||||
{{range $commit.Refs}}
|
||||
{{$refGroup := .RefGroup}}
|
||||
{{if eq $refGroup "pull"}}
|
||||
@ -56,7 +36,8 @@
|
||||
{{end}}
|
||||
{{end}}
|
||||
</span>
|
||||
<span class="author tw-flex tw-items-center tw-mr-2 tw-gap-1">
|
||||
|
||||
<span class="author flex-text-inline">
|
||||
{{$userName := $commit.Commit.Author.Name}}
|
||||
{{if $commit.User}}
|
||||
{{if and $commit.User.FullName DefaultShowFullName}}
|
||||
@ -69,7 +50,8 @@
|
||||
{{$userName}}
|
||||
{{end}}
|
||||
</span>
|
||||
<span class="time tw-flex tw-items-center">{{DateUtils.FullTime $commit.Date}}</span>
|
||||
|
||||
<span class="time flex-text-inline">{{DateUtils.FullTime $commit.Date}}</span>
|
||||
{{end}}
|
||||
</li>
|
||||
{{end}}
|
||||
|
@ -365,8 +365,9 @@
|
||||
{{if .Review}}{{$reviewType = .Review.Type}}{{end}}
|
||||
{{if not .OriginalAuthor}}
|
||||
{{/* Some timeline avatars need a offset to correctly align with their speech bubble.
|
||||
The condition depends on whether the comment has contents/attachments or reviews */}}
|
||||
<a class="timeline-avatar{{if or .Content .Attachments (and .Review .Review.CodeComments)}} timeline-avatar-offset{{end}}"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>
|
||||
The condition depends on whether the comment has contents/attachments,
|
||||
review's comment is also controlled/rendered by issue comment's Content field */}}
|
||||
<a class="timeline-avatar{{if or .Content .Attachments}} timeline-avatar-offset{{end}}"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}>
|
||||
{{ctx.AvatarUtils.Avatar .Poster 40}}
|
||||
</a>
|
||||
{{end}}
|
||||
|
@ -1,8 +1,9 @@
|
||||
<div class="latest-commit">
|
||||
{{if not .LatestCommit}}
|
||||
…
|
||||
{{else}}
|
||||
{{if .LatestCommitUser}}
|
||||
{{ctx.AvatarUtils.Avatar .LatestCommitUser 24 "tw-mr-1"}}
|
||||
{{ctx.AvatarUtils.Avatar .LatestCommitUser 24}}
|
||||
{{if and .LatestCommitUser.FullName DefaultShowFullName}}
|
||||
<a class="muted author-wrapper" title="{{.LatestCommitUser.FullName}}" href="{{.LatestCommitUser.HomeLink}}"><strong>{{.LatestCommitUser.FullName}}</strong></a>
|
||||
{{else}}
|
||||
@ -10,17 +11,15 @@
|
||||
{{end}}
|
||||
{{else}}
|
||||
{{if .LatestCommit.Author}}
|
||||
{{ctx.AvatarUtils.AvatarByEmail .LatestCommit.Author.Email .LatestCommit.Author.Name 24 "tw-mr-1"}}
|
||||
{{ctx.AvatarUtils.AvatarByEmail .LatestCommit.Author.Email .LatestCommit.Author.Name 24}}
|
||||
<span class="author-wrapper" title="{{.LatestCommit.Author.Name}}"><strong>{{.LatestCommit.Author.Name}}</strong></span>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<a rel="nofollow" class="ui sha label {{if .LatestCommit.Signature}} isSigned {{if .LatestCommitVerification.Verified}} isVerified{{if eq .LatestCommitVerification.TrustStatus "trusted"}}{{else if eq .LatestCommitVerification.TrustStatus "untrusted"}}Untrusted{{else}}Unmatched{{end}}{{else if .LatestCommitVerification.Warning}} isWarning{{end}}{{end}}" href="{{.RepoLink}}/commit/{{PathEscape .LatestCommit.ID.String}}">
|
||||
<span class="shortsha">{{ShortSha .LatestCommit.ID.String}}</span>
|
||||
{{if .LatestCommit.Signature}}
|
||||
{{template "repo/shabox_badge" dict "root" $ "verification" .LatestCommitVerification}}
|
||||
{{end}}
|
||||
</a>
|
||||
|
||||
{{template "repo/commit_sign_badge" dict "Commit" .LatestCommit "CommitBaseLink" (print .RepoLink "/commit") "CommitSignVerification" .LatestCommitVerification}}
|
||||
|
||||
{{template "repo/commit_statuses" dict "Status" .LatestCommitStatus "Statuses" .LatestCommitStatuses}}
|
||||
|
||||
{{$commitLink:= printf "%s/commit/%s" .RepoLink (PathEscape .LatestCommit.ID.String)}}
|
||||
<span class="grey commit-summary" title="{{.LatestCommit.Summary}}"><span class="message-wrapper">{{ctx.RenderUtils.RenderCommitMessageLinkSubject .LatestCommit.Message $commitLink ($.Repository.ComposeMetas ctx)}}</span>
|
||||
{{if IsMultilineCommitMessage .LatestCommit.Message}}
|
||||
@ -29,3 +28,4 @@
|
||||
{{end}}
|
||||
</span>
|
||||
{{end}}
|
||||
</div>
|
||||
|
@ -733,7 +733,7 @@
|
||||
<span class="field">
|
||||
{{if .CodeIndexerStatus}}
|
||||
<a rel="nofollow" class="ui sha label" href="{{.RepoLink}}/commit/{{.CodeIndexerStatus.CommitSha}}">
|
||||
<span class="shortsha">{{ShortSha .CodeIndexerStatus.CommitSha}}</span>
|
||||
{{ShortSha .CodeIndexerStatus.CommitSha}}
|
||||
</a>
|
||||
{{else}}
|
||||
<span>{{ctx.Locale.Tr "repo.settings.admin_indexer_unindexed"}}</span>
|
||||
@ -752,7 +752,7 @@
|
||||
<span class="field">
|
||||
{{if and .StatsIndexerStatus .StatsIndexerStatus.CommitSha}}
|
||||
<a rel="nofollow" class="ui sha label" href="{{.RepoLink}}/commit/{{.StatsIndexerStatus.CommitSha}}">
|
||||
<span class="shortsha">{{ShortSha .StatsIndexerStatus.CommitSha}}</span>
|
||||
{{ShortSha .StatsIndexerStatus.CommitSha}}
|
||||
</a>
|
||||
{{else}}
|
||||
<span>{{ctx.Locale.Tr "repo.settings.admin_indexer_unindexed"}}</span>
|
||||
|
@ -1,15 +0,0 @@
|
||||
<div class="ui detail icon button">
|
||||
{{if .verification.Verified}}
|
||||
<div title="{{if eq .verification.TrustStatus "trusted"}}{{else if eq .verification.TrustStatus "untrusted"}}{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user"}}: {{else}}{{ctx.Locale.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}: {{end}}{{.verification.Reason}}">
|
||||
{{if ne .verification.SigningUser.ID 0}}
|
||||
{{svg "gitea-lock"}}
|
||||
{{ctx.AvatarUtils.Avatar .verification.SigningUser 16 "signature"}}
|
||||
{{else}}
|
||||
<span title="{{ctx.Locale.Tr "gpg.default_key"}}">{{svg "gitea-lock-cog"}}</span>
|
||||
{{ctx.AvatarUtils.AvatarByEmail .verification.SigningEmail "" 16 "signature"}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<span title="{{ctx.Locale.Tr .verification.Reason}}">{{svg "gitea-unlock"}}</span>
|
||||
{{end}}
|
||||
</div>
|
@ -12,9 +12,7 @@
|
||||
|
||||
{{if not .ReadmeInList}}
|
||||
<div id="repo-file-commit-box" class="ui segment list-header tw-mb-4 tw-flex tw-justify-between">
|
||||
<div class="latest-commit">
|
||||
{{template "repo/latest_commit" .}}
|
||||
</div>
|
||||
{{template "repo/latest_commit" .}}
|
||||
{{if .LatestCommit}}
|
||||
{{if .LatestCommit.Committer}}
|
||||
<div class="text grey age">
|
||||
|
@ -1,7 +1,7 @@
|
||||
{{/* use grid layout, still use the old ID because there are many other CSS styles depending on this ID */}}
|
||||
<div id="repo-files-table" {{if .HasFilesWithoutLatestCommit}}hx-indicator="#repo-files-table .repo-file-cell.message" hx-trigger="load" hx-swap="morph" hx-post="{{.LastCommitLoaderURL}}"{{end}}>
|
||||
<div class="repo-file-line repo-file-last-commit">
|
||||
<div class="latest-commit">{{template "repo/latest_commit" .}}</div>
|
||||
{{template "repo/latest_commit" .}}
|
||||
<div>{{if and .LatestCommit .LatestCommit.Committer}}{{DateUtils.TimeSince .LatestCommit.Committer.When}}{{end}}</div>
|
||||
</div>
|
||||
{{if .HasParentPath}}
|
||||
|
@ -25,30 +25,10 @@
|
||||
{{end}}
|
||||
</span>
|
||||
</div>
|
||||
{{if or .TotalTrackedTime .Assignees .NumComments}}
|
||||
<div class="flex-item-trailing">
|
||||
{{if .TotalTrackedTime}}
|
||||
<div class="text grey flex-text-block">
|
||||
{{svg "octicon-clock" 16}}
|
||||
{{.TotalTrackedTime | Sec2Time}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Assignees}}
|
||||
<div class="text grey">
|
||||
{{range .Assignees}}
|
||||
<a class="ui assignee tw-no-underline" href="{{.HomeLink}}" data-tooltip-content="{{.GetDisplayName}}">
|
||||
{{ctx.AvatarUtils.Avatar . 20}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .NumComments}}
|
||||
<div class="text grey">
|
||||
<a class="tw-no-underline muted flex-text-block" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}">
|
||||
{{svg "octicon-comment" 16}}{{.NumComments}}
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .TotalTrackedTime}}
|
||||
<div class="text grey flex-text-block">
|
||||
{{svg "octicon-clock" 16}}
|
||||
{{.TotalTrackedTime | Sec2Time}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
@ -152,6 +132,26 @@
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{if or .Assignees .NumComments}}
|
||||
<div class="flex-item-trailing">
|
||||
{{if .Assignees}}
|
||||
<div class="text grey">
|
||||
{{range .Assignees}}
|
||||
<a class="ui assignee tw-no-underline" href="{{.HomeLink}}" data-tooltip-content="{{.GetDisplayName}}">
|
||||
{{ctx.AvatarUtils.Avatar . 20}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .NumComments}}
|
||||
<div class="text grey">
|
||||
<a class="tw-no-underline muted flex-text-block" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}">
|
||||
{{svg "octicon-comment" 16}}{{.NumComments}}
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .IssueIndexerUnavailable}}
|
||||
|
@ -92,6 +92,9 @@
|
||||
</li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if .ShowMoreOrgs}}
|
||||
<li><a class="tw-align-center" href="{{.ContextUser.HomeLink}}?tab=organizations" data-tooltip-content="{{ctx.Locale.Tr "user.show_more"}}">{{svg "octicon-kebab-horizontal" 28 "icon tw-p-1"}}</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</li>
|
||||
{{end}}
|
||||
|
@ -27,6 +27,8 @@
|
||||
{{template "repo/user_cards" .}}
|
||||
{{else if eq .TabName "overview"}}
|
||||
<div id="readme_profile" class="markup">{{.ProfileReadme}}</div>
|
||||
{{else if eq .TabName "organizations"}}
|
||||
{{template "repo/user_cards" .}}
|
||||
{{else}}
|
||||
{{template "shared/repo_search" .}}
|
||||
{{template "explore/repo_list" .}}
|
||||
|
410
tests/integration/actions_job_test.go
Normal file
410
tests/integration/actions_job_test.go
Normal file
@ -0,0 +1,410 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestJobWithNeeds(t *testing.T) {
|
||||
testCases := []struct {
|
||||
treePath string
|
||||
fileContent string
|
||||
outcomes map[string]*mockTaskOutcome
|
||||
expectedStatuses map[string]string
|
||||
}{
|
||||
{
|
||||
treePath: ".gitea/workflows/job-with-needs.yml",
|
||||
fileContent: `name: job-with-needs
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/job-with-needs.yml'
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job1
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [job1]
|
||||
steps:
|
||||
- run: echo job2
|
||||
`,
|
||||
outcomes: map[string]*mockTaskOutcome{
|
||||
"job1": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
},
|
||||
"job2": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
},
|
||||
},
|
||||
expectedStatuses: map[string]string{
|
||||
"job1": actions_model.StatusSuccess.String(),
|
||||
"job2": actions_model.StatusSuccess.String(),
|
||||
},
|
||||
},
|
||||
{
|
||||
treePath: ".gitea/workflows/job-with-needs-fail.yml",
|
||||
fileContent: `name: job-with-needs-fail
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/job-with-needs-fail.yml'
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job1
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [job1]
|
||||
steps:
|
||||
- run: echo job2
|
||||
`,
|
||||
outcomes: map[string]*mockTaskOutcome{
|
||||
"job1": {
|
||||
result: runnerv1.Result_RESULT_FAILURE,
|
||||
},
|
||||
},
|
||||
expectedStatuses: map[string]string{
|
||||
"job1": actions_model.StatusFailure.String(),
|
||||
"job2": actions_model.StatusSkipped.String(),
|
||||
},
|
||||
},
|
||||
{
|
||||
treePath: ".gitea/workflows/job-with-needs-fail-if.yml",
|
||||
fileContent: `name: job-with-needs-fail-if
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/job-with-needs-fail-if.yml'
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job1
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ always() }}
|
||||
needs: [job1]
|
||||
steps:
|
||||
- run: echo job2
|
||||
`,
|
||||
outcomes: map[string]*mockTaskOutcome{
|
||||
"job1": {
|
||||
result: runnerv1.Result_RESULT_FAILURE,
|
||||
},
|
||||
"job2": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
},
|
||||
},
|
||||
expectedStatuses: map[string]string{
|
||||
"job1": actions_model.StatusFailure.String(),
|
||||
"job2": actions_model.StatusSuccess.String(),
|
||||
},
|
||||
},
|
||||
}
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
session := loginUser(t, user2.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
apiRepo := createActionsTestRepo(t, token, "actions-jobs-with-needs", false)
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"})
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) {
|
||||
// create the workflow file
|
||||
opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent)
|
||||
fileResp := createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts)
|
||||
|
||||
// fetch and execute task
|
||||
for i := 0; i < len(tc.outcomes); i++ {
|
||||
task := runner.fetchTask(t)
|
||||
jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id)
|
||||
outcome := tc.outcomes[jobName]
|
||||
assert.NotNil(t, outcome)
|
||||
runner.execTask(t, task, outcome)
|
||||
}
|
||||
|
||||
// check result
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", user2.Name, apiRepo.Name)).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var actionTaskRespAfter api.ActionTaskResponse
|
||||
DecodeJSON(t, resp, &actionTaskRespAfter)
|
||||
for _, apiTask := range actionTaskRespAfter.Entries {
|
||||
if apiTask.HeadSHA != fileResp.Commit.SHA {
|
||||
continue
|
||||
}
|
||||
status := apiTask.Status
|
||||
assert.Equal(t, status, tc.expectedStatuses[apiTask.Name])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
doAPIDeleteRepository(httpContext)(t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestJobNeedsMatrix(t *testing.T) {
|
||||
testCases := []struct {
|
||||
treePath string
|
||||
fileContent string
|
||||
outcomes map[string]*mockTaskOutcome
|
||||
expectedTaskNeeds map[string]*runnerv1.TaskNeed // jobID => TaskNeed
|
||||
}{
|
||||
{
|
||||
treePath: ".gitea/workflows/jobs-outputs-with-matrix.yml",
|
||||
fileContent: `name: jobs-outputs-with-matrix
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/jobs-outputs-with-matrix.yml'
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
output_1: ${{ steps.gen_output.outputs.output_1 }}
|
||||
output_2: ${{ steps.gen_output.outputs.output_2 }}
|
||||
output_3: ${{ steps.gen_output.outputs.output_3 }}
|
||||
strategy:
|
||||
matrix:
|
||||
version: [1, 2, 3]
|
||||
steps:
|
||||
- name: Generate output
|
||||
id: gen_output
|
||||
run: |
|
||||
version="${{ matrix.version }}"
|
||||
echo "output_${version}=${version}" >> "$GITHUB_OUTPUT"
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [job1]
|
||||
steps:
|
||||
- run: echo '${{ toJSON(needs.job1.outputs) }}'
|
||||
`,
|
||||
outcomes: map[string]*mockTaskOutcome{
|
||||
"job1 (1)": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
outputs: map[string]string{
|
||||
"output_1": "1",
|
||||
"output_2": "",
|
||||
"output_3": "",
|
||||
},
|
||||
},
|
||||
"job1 (2)": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
outputs: map[string]string{
|
||||
"output_1": "",
|
||||
"output_2": "2",
|
||||
"output_3": "",
|
||||
},
|
||||
},
|
||||
"job1 (3)": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
outputs: map[string]string{
|
||||
"output_1": "",
|
||||
"output_2": "",
|
||||
"output_3": "3",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedTaskNeeds: map[string]*runnerv1.TaskNeed{
|
||||
"job1": {
|
||||
Result: runnerv1.Result_RESULT_SUCCESS,
|
||||
Outputs: map[string]string{
|
||||
"output_1": "1",
|
||||
"output_2": "2",
|
||||
"output_3": "3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
treePath: ".gitea/workflows/jobs-outputs-with-matrix-failure.yml",
|
||||
fileContent: `name: jobs-outputs-with-matrix-failure
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/jobs-outputs-with-matrix-failure.yml'
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
output_1: ${{ steps.gen_output.outputs.output_1 }}
|
||||
output_2: ${{ steps.gen_output.outputs.output_2 }}
|
||||
output_3: ${{ steps.gen_output.outputs.output_3 }}
|
||||
strategy:
|
||||
matrix:
|
||||
version: [1, 2, 3]
|
||||
steps:
|
||||
- name: Generate output
|
||||
id: gen_output
|
||||
run: |
|
||||
version="${{ matrix.version }}"
|
||||
echo "output_${version}=${version}" >> "$GITHUB_OUTPUT"
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ always() }}
|
||||
needs: [job1]
|
||||
steps:
|
||||
- run: echo '${{ toJSON(needs.job1.outputs) }}'
|
||||
`,
|
||||
outcomes: map[string]*mockTaskOutcome{
|
||||
"job1 (1)": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
outputs: map[string]string{
|
||||
"output_1": "1",
|
||||
"output_2": "",
|
||||
"output_3": "",
|
||||
},
|
||||
},
|
||||
"job1 (2)": {
|
||||
result: runnerv1.Result_RESULT_FAILURE,
|
||||
outputs: map[string]string{
|
||||
"output_1": "",
|
||||
"output_2": "",
|
||||
"output_3": "",
|
||||
},
|
||||
},
|
||||
"job1 (3)": {
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
outputs: map[string]string{
|
||||
"output_1": "",
|
||||
"output_2": "",
|
||||
"output_3": "3",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedTaskNeeds: map[string]*runnerv1.TaskNeed{
|
||||
"job1": {
|
||||
Result: runnerv1.Result_RESULT_FAILURE,
|
||||
Outputs: map[string]string{
|
||||
"output_1": "1",
|
||||
"output_2": "",
|
||||
"output_3": "3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
session := loginUser(t, user2.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
apiRepo := createActionsTestRepo(t, token, "actions-jobs-outputs-with-matrix", false)
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, user2.Name, apiRepo.Name, "mock-runner", []string{"ubuntu-latest"})
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) {
|
||||
opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent)
|
||||
createWorkflowFile(t, token, user2.Name, apiRepo.Name, tc.treePath, opts)
|
||||
|
||||
for i := 0; i < len(tc.outcomes); i++ {
|
||||
task := runner.fetchTask(t)
|
||||
jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id)
|
||||
outcome := tc.outcomes[jobName]
|
||||
assert.NotNil(t, outcome)
|
||||
runner.execTask(t, task, outcome)
|
||||
}
|
||||
|
||||
task := runner.fetchTask(t)
|
||||
actualTaskNeeds := task.Needs
|
||||
assert.Len(t, actualTaskNeeds, len(tc.expectedTaskNeeds))
|
||||
for jobID, tn := range tc.expectedTaskNeeds {
|
||||
actualNeed := actualTaskNeeds[jobID]
|
||||
assert.Equal(t, tn.Result, actualNeed.Result)
|
||||
assert.Len(t, actualNeed.Outputs, len(tn.Outputs))
|
||||
for outputKey, outputValue := range tn.Outputs {
|
||||
assert.Equal(t, outputValue, actualNeed.Outputs[outputKey])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
httpContext := NewAPITestContext(t, user2.Name, apiRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
doAPIDeleteRepository(httpContext)(t)
|
||||
})
|
||||
}
|
||||
|
||||
func createActionsTestRepo(t *testing.T, authToken, repoName string, isPrivate bool) *api.Repository {
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
|
||||
Name: repoName,
|
||||
Private: isPrivate,
|
||||
Readme: "Default",
|
||||
AutoInit: true,
|
||||
DefaultBranch: "main",
|
||||
}).AddTokenAuth(authToken)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
var apiRepo api.Repository
|
||||
DecodeJSON(t, resp, &apiRepo)
|
||||
return &apiRepo
|
||||
}
|
||||
|
||||
func getWorkflowCreateFileOptions(u *user_model.User, branch, msg, content string) *api.CreateFileOptions {
|
||||
return &api.CreateFileOptions{
|
||||
FileOptions: api.FileOptions{
|
||||
BranchName: branch,
|
||||
Message: msg,
|
||||
Author: api.Identity{
|
||||
Name: u.Name,
|
||||
Email: u.Email,
|
||||
},
|
||||
Committer: api.Identity{
|
||||
Name: u.Name,
|
||||
Email: u.Email,
|
||||
},
|
||||
Dates: api.CommitDateOptions{
|
||||
Author: time.Now(),
|
||||
Committer: time.Now(),
|
||||
},
|
||||
},
|
||||
ContentBase64: base64.StdEncoding.EncodeToString([]byte(content)),
|
||||
}
|
||||
}
|
||||
|
||||
func createWorkflowFile(t *testing.T, authToken, ownerName, repoName, treePath string, opts *api.CreateFileOptions) *api.FileResponse {
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", ownerName, repoName, treePath), opts).
|
||||
AddTokenAuth(authToken)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
var fileResponse api.FileResponse
|
||||
DecodeJSON(t, resp, &fileResponse)
|
||||
return &fileResponse
|
||||
}
|
||||
|
||||
// getTaskJobNameByTaskID get the job name of the task by task ID
|
||||
// there is currently not an API for querying a task by ID so we have to list all the tasks
|
||||
func getTaskJobNameByTaskID(t *testing.T, authToken, ownerName, repoName string, taskID int64) string {
|
||||
// FIXME: we may need to query several pages
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/tasks", ownerName, repoName)).
|
||||
AddTokenAuth(authToken)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var taskRespBefore api.ActionTaskResponse
|
||||
DecodeJSON(t, resp, &taskRespBefore)
|
||||
for _, apiTask := range taskRespBefore.Entries {
|
||||
if apiTask.ID == taskID {
|
||||
return apiTask.Name
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
159
tests/integration/actions_log_test.go
Normal file
159
tests/integration/actions_log_test.go
Normal file
@ -0,0 +1,159 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func TestDownloadTaskLogs(t *testing.T) {
|
||||
now := time.Now()
|
||||
testCases := []struct {
|
||||
treePath string
|
||||
fileContent string
|
||||
outcome *mockTaskOutcome
|
||||
zstdEnabled bool
|
||||
}{
|
||||
{
|
||||
treePath: ".gitea/workflows/download-task-logs-zstd.yml",
|
||||
fileContent: `name: download-task-logs-zstd
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/download-task-logs-zstd.yml'
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job1 with zstd enabled
|
||||
`,
|
||||
outcome: &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
logRows: []*runnerv1.LogRow{
|
||||
{
|
||||
Time: timestamppb.New(now.Add(1 * time.Second)),
|
||||
Content: " \U0001F433 docker create image",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(2 * time.Second)),
|
||||
Content: "job1 zstd enabled",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(3 * time.Second)),
|
||||
Content: "\U0001F3C1 Job succeeded",
|
||||
},
|
||||
},
|
||||
},
|
||||
zstdEnabled: true,
|
||||
},
|
||||
{
|
||||
treePath: ".gitea/workflows/download-task-logs-no-zstd.yml",
|
||||
fileContent: `name: download-task-logs-no-zstd
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/download-task-logs-no-zstd.yml'
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job1 with zstd disabled
|
||||
`,
|
||||
outcome: &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
logRows: []*runnerv1.LogRow{
|
||||
{
|
||||
Time: timestamppb.New(now.Add(4 * time.Second)),
|
||||
Content: " \U0001F433 docker create image",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(5 * time.Second)),
|
||||
Content: "job1 zstd disabled",
|
||||
},
|
||||
{
|
||||
Time: timestamppb.New(now.Add(6 * time.Second)),
|
||||
Content: "\U0001F3C1 Job succeeded",
|
||||
},
|
||||
},
|
||||
},
|
||||
zstdEnabled: false,
|
||||
},
|
||||
}
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
session := loginUser(t, user2.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
apiRepo := createActionsTestRepo(t, token, "actions-download-task-logs", false)
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner", []string{"ubuntu-latest"})
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("test %s", tc.treePath), func(t *testing.T) {
|
||||
var resetFunc func()
|
||||
if tc.zstdEnabled {
|
||||
resetFunc = test.MockVariableValue(&setting.Actions.LogCompression, "zstd")
|
||||
assert.True(t, setting.Actions.LogCompression.IsZstd())
|
||||
} else {
|
||||
resetFunc = test.MockVariableValue(&setting.Actions.LogCompression, "none")
|
||||
assert.False(t, setting.Actions.LogCompression.IsZstd())
|
||||
}
|
||||
|
||||
// create the workflow file
|
||||
opts := getWorkflowCreateFileOptions(user2, repo.DefaultBranch, fmt.Sprintf("create %s", tc.treePath), tc.fileContent)
|
||||
createWorkflowFile(t, token, user2.Name, repo.Name, tc.treePath, opts)
|
||||
|
||||
// fetch and execute task
|
||||
task := runner.fetchTask(t)
|
||||
runner.execTask(t, task, tc.outcome)
|
||||
|
||||
// check whether the log file exists
|
||||
logFileName := fmt.Sprintf("%s/%02x/%d.log", repo.FullName(), task.Id%256, task.Id)
|
||||
if setting.Actions.LogCompression.IsZstd() {
|
||||
logFileName += ".zst"
|
||||
}
|
||||
_, err := storage.Actions.Stat(logFileName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// download task logs and check content
|
||||
runIndex := task.Context.GetFields()["run_number"].GetStringValue()
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/0/logs", user2.Name, repo.Name, runIndex)).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
logTextLines := strings.Split(strings.TrimSpace(resp.Body.String()), "\n")
|
||||
assert.Len(t, logTextLines, len(tc.outcome.logRows))
|
||||
for idx, lr := range tc.outcome.logRows {
|
||||
assert.Equal(
|
||||
t,
|
||||
fmt.Sprintf("%s %s", lr.Time.AsTime().Format("2006-01-02T15:04:05.0000000Z07:00"), lr.Content),
|
||||
logTextLines[idx],
|
||||
)
|
||||
}
|
||||
|
||||
resetFunc()
|
||||
})
|
||||
}
|
||||
|
||||
httpContext := NewAPITestContext(t, user2.Name, repo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
doAPIDeleteRepository(httpContext)(t)
|
||||
})
|
||||
}
|
157
tests/integration/actions_runner_test.go
Normal file
157
tests/integration/actions_runner_test.go
Normal file
@ -0,0 +1,157 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
|
||||
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
|
||||
"connectrpc.com/connect"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type mockRunner struct {
|
||||
client *mockRunnerClient
|
||||
}
|
||||
|
||||
type mockRunnerClient struct {
|
||||
pingServiceClient pingv1connect.PingServiceClient
|
||||
runnerServiceClient runnerv1connect.RunnerServiceClient
|
||||
}
|
||||
|
||||
func newMockRunner() *mockRunner {
|
||||
client := newMockRunnerClient("", "")
|
||||
return &mockRunner{client: client}
|
||||
}
|
||||
|
||||
func newMockRunnerClient(uuid, token string) *mockRunnerClient {
|
||||
baseURL := fmt.Sprintf("%sapi/actions", setting.AppURL)
|
||||
|
||||
opt := connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
|
||||
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
|
||||
if uuid != "" {
|
||||
req.Header().Set("x-runner-uuid", uuid)
|
||||
}
|
||||
if token != "" {
|
||||
req.Header().Set("x-runner-token", token)
|
||||
}
|
||||
return next(ctx, req)
|
||||
}
|
||||
}))
|
||||
|
||||
client := &mockRunnerClient{
|
||||
pingServiceClient: pingv1connect.NewPingServiceClient(http.DefaultClient, baseURL, opt),
|
||||
runnerServiceClient: runnerv1connect.NewRunnerServiceClient(http.DefaultClient, baseURL, opt),
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func (r *mockRunner) doPing(t *testing.T) {
|
||||
resp, err := r.client.pingServiceClient.Ping(context.Background(), connect.NewRequest(&pingv1.PingRequest{
|
||||
Data: "mock-runner",
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Hello, mock-runner!", resp.Msg.Data)
|
||||
}
|
||||
|
||||
func (r *mockRunner) doRegister(t *testing.T, name, token string, labels []string) {
|
||||
r.doPing(t)
|
||||
resp, err := r.client.runnerServiceClient.Register(context.Background(), connect.NewRequest(&runnerv1.RegisterRequest{
|
||||
Name: name,
|
||||
Token: token,
|
||||
Version: "mock-runner-version",
|
||||
Labels: labels,
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
r.client = newMockRunnerClient(resp.Msg.Runner.Uuid, resp.Msg.Runner.Token)
|
||||
}
|
||||
|
||||
func (r *mockRunner) registerAsRepoRunner(t *testing.T, ownerName, repoName, runnerName string, labels []string) {
|
||||
session := loginUser(t, ownerName)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/registration-token", ownerName, repoName)).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var registrationToken struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
DecodeJSON(t, resp, ®istrationToken)
|
||||
r.doRegister(t, runnerName, registrationToken.Token, labels)
|
||||
}
|
||||
|
||||
func (r *mockRunner) fetchTask(t *testing.T, timeout ...time.Duration) *runnerv1.Task {
|
||||
fetchTimeout := 10 * time.Second
|
||||
if len(timeout) > 0 {
|
||||
fetchTimeout = timeout[0]
|
||||
}
|
||||
ddl := time.Now().Add(fetchTimeout)
|
||||
var task *runnerv1.Task
|
||||
for time.Now().Before(ddl) {
|
||||
resp, err := r.client.runnerServiceClient.FetchTask(context.Background(), connect.NewRequest(&runnerv1.FetchTaskRequest{
|
||||
TasksVersion: 0,
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
if resp.Msg.Task != nil {
|
||||
task = resp.Msg.Task
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
assert.NotNil(t, task, "failed to fetch a task")
|
||||
return task
|
||||
}
|
||||
|
||||
type mockTaskOutcome struct {
|
||||
result runnerv1.Result
|
||||
outputs map[string]string
|
||||
logRows []*runnerv1.LogRow
|
||||
execTime time.Duration
|
||||
}
|
||||
|
||||
func (r *mockRunner) execTask(t *testing.T, task *runnerv1.Task, outcome *mockTaskOutcome) {
|
||||
for idx, lr := range outcome.logRows {
|
||||
resp, err := r.client.runnerServiceClient.UpdateLog(context.Background(), connect.NewRequest(&runnerv1.UpdateLogRequest{
|
||||
TaskId: task.Id,
|
||||
Index: int64(idx),
|
||||
Rows: []*runnerv1.LogRow{lr},
|
||||
NoMore: idx == len(outcome.logRows)-1,
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, idx+1, resp.Msg.AckIndex)
|
||||
}
|
||||
sentOutputKeys := make([]string, 0, len(outcome.outputs))
|
||||
for outputKey, outputValue := range outcome.outputs {
|
||||
resp, err := r.client.runnerServiceClient.UpdateTask(context.Background(), connect.NewRequest(&runnerv1.UpdateTaskRequest{
|
||||
State: &runnerv1.TaskState{
|
||||
Id: task.Id,
|
||||
Result: runnerv1.Result_RESULT_UNSPECIFIED,
|
||||
},
|
||||
Outputs: map[string]string{outputKey: outputValue},
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
sentOutputKeys = append(sentOutputKeys, outputKey)
|
||||
assert.ElementsMatch(t, sentOutputKeys, resp.Msg.SentOutputs)
|
||||
}
|
||||
time.Sleep(outcome.execTime)
|
||||
resp, err := r.client.runnerServiceClient.UpdateTask(context.Background(), connect.NewRequest(&runnerv1.UpdateTaskRequest{
|
||||
State: &runnerv1.TaskState{
|
||||
Id: task.Id,
|
||||
Result: outcome.result,
|
||||
StoppedAt: timestamppb.Now(),
|
||||
},
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, outcome.result, resp.Msg.State.Result)
|
||||
}
|
@ -117,27 +117,33 @@ func TestAPIAddIssueLabels(t *testing.T) {
|
||||
func TestAPIAddIssueLabelsWithLabelNames(t *testing.T) {
|
||||
assert.NoError(t, unittest.LoadFixtures())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{RepoID: repo.ID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 6, RepoID: repo.ID})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
repoLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 10, RepoID: repo.ID})
|
||||
orgLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 4, OrgID: owner.ID})
|
||||
|
||||
session := loginUser(t, owner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels",
|
||||
repo.OwnerName, repo.Name, issue.Index)
|
||||
user1Session := loginUser(t, "user1")
|
||||
token := getTokenForLoggedInUser(t, user1Session, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
// add the org label and the repo label to the issue
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", owner.Name, repo.Name, issue.Index)
|
||||
req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{
|
||||
Labels: []any{"label1", "label2"},
|
||||
Labels: []any{repoLabel.Name, orgLabel.Name},
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var apiLabels []*api.Label
|
||||
DecodeJSON(t, resp, &apiLabels)
|
||||
assert.Len(t, apiLabels, unittest.GetCount(t, &issues_model.IssueLabel{IssueID: issue.ID}))
|
||||
|
||||
var apiLabelNames []string
|
||||
for _, label := range apiLabels {
|
||||
apiLabelNames = append(apiLabelNames, label.Name)
|
||||
}
|
||||
assert.ElementsMatch(t, apiLabelNames, []string{"label1", "label2"})
|
||||
assert.ElementsMatch(t, apiLabelNames, []string{repoLabel.Name, orgLabel.Name})
|
||||
|
||||
// delete labels
|
||||
req = NewRequest(t, "DELETE", urlStr).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
}
|
||||
|
||||
func TestAPIReplaceIssueLabels(t *testing.T) {
|
||||
|
@ -332,7 +332,7 @@ func TestLDAPUserSyncWithGroupFilter(t *testing.T) {
|
||||
|
||||
// Assert members of LDAP group "cn=git" are added
|
||||
for _, gitLDAPUser := range te.gitLDAPUsers {
|
||||
unittest.BeanExists(t, &user_model.User{
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{
|
||||
Name: gitLDAPUser.UserName,
|
||||
})
|
||||
}
|
||||
|
@ -274,7 +274,8 @@ func TestOrgTeamEmailInviteRedirectsNewUserWithActivation(t *testing.T) {
|
||||
user, err := user_model.GetUserByName(db.DefaultContext, "doesnotexist")
|
||||
assert.NoError(t, err)
|
||||
|
||||
activateURL := fmt.Sprintf("/user/activate?code=%s", user.GenerateEmailActivateCode("doesnotexist@example.com"))
|
||||
activationCode := user_model.GenerateUserTimeLimitCode(&user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, user)
|
||||
activateURL := fmt.Sprintf("/user/activate?code=%s", activationCode)
|
||||
req = NewRequestWithValues(t, "POST", activateURL, map[string]string{
|
||||
"password": "examplePassword!1",
|
||||
})
|
||||
|
@ -12,6 +12,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
@ -82,6 +84,30 @@ func testPullCreateDirectly(t *testing.T, session *TestSession, baseRepoOwner, b
|
||||
return resp
|
||||
}
|
||||
|
||||
func testPullCreateFailure(t *testing.T, session *TestSession, baseRepoOwner, baseRepoName, baseBranch, headRepoOwner, headRepoName, headBranch, title string) *httptest.ResponseRecorder {
|
||||
headCompare := headBranch
|
||||
if headRepoOwner != "" {
|
||||
if headRepoName != "" {
|
||||
headCompare = fmt.Sprintf("%s/%s:%s", headRepoOwner, headRepoName, headBranch)
|
||||
} else {
|
||||
headCompare = fmt.Sprintf("%s:%s", headRepoOwner, headBranch)
|
||||
}
|
||||
}
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/compare/%s...%s", baseRepoOwner, baseRepoName, baseBranch, headCompare))
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// Submit the form for creating the pull
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
link, exists := htmlDoc.doc.Find("form.ui.form").Attr("action")
|
||||
assert.True(t, exists, "The template has changed")
|
||||
req = NewRequestWithValues(t, "POST", link, map[string]string{
|
||||
"_csrf": htmlDoc.GetCSRF(),
|
||||
"title": title,
|
||||
})
|
||||
resp = session.MakeRequest(t, req, http.StatusBadRequest)
|
||||
return resp
|
||||
}
|
||||
|
||||
func TestPullCreate(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
session := loginUser(t, "user1")
|
||||
@ -226,3 +252,64 @@ func TestPullCreatePrFromBaseToFork(t *testing.T) {
|
||||
assert.Regexp(t, "^/user1/repo1/pulls/[0-9]*$", url)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateAgitPullWithReadPermission(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
dstPath := t.TempDir()
|
||||
|
||||
u.Path = "user2/repo1.git"
|
||||
u.User = url.UserPassword("user4", userPassword)
|
||||
|
||||
t.Run("Clone", doGitClone(dstPath, u))
|
||||
|
||||
t.Run("add commit", doGitAddSomeCommits(dstPath, "master"))
|
||||
|
||||
t.Run("do agit pull create", func(t *testing.T) {
|
||||
err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master", "-o").AddDynamicArguments("topic=" + "test-topic").Run(&git.RunOpts{Dir: dstPath})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
Setup: user2 has repository, user1 forks it
|
||||
---
|
||||
|
||||
1. User2 blocks User1
|
||||
2. User1 adds changes to fork
|
||||
3. User1 attempts to create a pull request
|
||||
4. User1 sees alert that the action is not allowed because of the block
|
||||
*/
|
||||
func TestCreatePullWhenBlocked(t *testing.T) {
|
||||
RepoOwner := "user2"
|
||||
ForkOwner := "user16"
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
// Setup
|
||||
// User1 forks repo1 from User2
|
||||
sessionFork := loginUser(t, ForkOwner)
|
||||
testRepoFork(t, sessionFork, RepoOwner, "repo1", ForkOwner, "forkrepo1", "")
|
||||
|
||||
// 1. User2 blocks user1
|
||||
// sessionBase := loginUser(t, "user2")
|
||||
token := getUserToken(t, RepoOwner, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/blocks/%s", ForkOwner)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/blocks/%s", ForkOwner)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// 2. User1 adds changes to fork
|
||||
testEditFile(t, sessionFork, ForkOwner, "forkrepo1", "master", "README.md", "Hello, World (Edited)\n")
|
||||
|
||||
// 3. User1 attempts to create a pull request
|
||||
testPullCreateFailure(t, sessionFork, RepoOwner, "repo1", "master", ForkOwner, "forkrepo1", "master", "This is a pull title")
|
||||
|
||||
// Teardown
|
||||
// Unblock user
|
||||
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/blocks/%s", ForkOwner)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
})
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ func TestPullView_CodeOwner(t *testing.T) {
|
||||
testPullCreate(t, session, "user2", "test_codeowner", false, repo.DefaultBranch, "codeowner-basebranch", "Test Pull Request")
|
||||
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: repo.ID, HeadBranch: "codeowner-basebranch"})
|
||||
unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 5})
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 5})
|
||||
assert.NoError(t, pr.LoadIssue(db.DefaultContext))
|
||||
|
||||
err := issue_service.ChangeTitle(db.DefaultContext, pr.Issue, user2, "[WIP] Test Pull Request")
|
||||
@ -139,7 +139,7 @@ func TestPullView_CodeOwner(t *testing.T) {
|
||||
testPullCreate(t, session, "user2", "test_codeowner", false, repo.DefaultBranch, "codeowner-basebranch2", "Test Pull Request2")
|
||||
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadBranch: "codeowner-basebranch2"})
|
||||
unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8})
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8})
|
||||
})
|
||||
|
||||
t.Run("Forked Repo Pull Request", func(t *testing.T) {
|
||||
@ -169,13 +169,13 @@ func TestPullView_CodeOwner(t *testing.T) {
|
||||
testPullCreateDirectly(t, session, "user5", "test_codeowner", forkedRepo.DefaultBranch, "", "", "codeowner-basebranch-forked", "Test Pull Request on Forked Repository")
|
||||
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: forkedRepo.ID, HeadBranch: "codeowner-basebranch-forked"})
|
||||
unittest.AssertExistsIf(t, false, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8})
|
||||
unittest.AssertNotExistsBean(t, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8})
|
||||
|
||||
// create a pull request to base repository, code reviewers should be mentioned
|
||||
testPullCreateDirectly(t, session, repo.OwnerName, repo.Name, repo.DefaultBranch, forkedRepo.OwnerName, forkedRepo.Name, "codeowner-basebranch-forked", "Test Pull Request3")
|
||||
|
||||
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: repo.ID, HeadRepoID: forkedRepo.ID, HeadBranch: "codeowner-basebranch-forked"})
|
||||
unittest.AssertExistsIf(t, true, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8})
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Review{IssueID: pr.IssueID, Type: issues_model.ReviewTypeRequest, ReviewerID: 8})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ func TestRepoCommits(t *testing.T) {
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href")
|
||||
commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href")
|
||||
assert.True(t, exists)
|
||||
assert.NotEmpty(t, commitURL)
|
||||
}
|
||||
@ -46,7 +46,7 @@ func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) {
|
||||
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
// Get first commit URL
|
||||
commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href")
|
||||
commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href")
|
||||
assert.True(t, exists)
|
||||
assert.NotEmpty(t, commitURL)
|
||||
|
||||
@ -64,7 +64,7 @@ func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) {
|
||||
|
||||
doc = NewHTMLParser(t, resp.Body)
|
||||
// Check if commit status is displayed in message column (.tippy-target to ignore the tippy trigger)
|
||||
sel := doc.doc.Find("#commits-table tbody tr td.message .tippy-target .commit-status")
|
||||
sel := doc.doc.Find("#commits-table .message .tippy-target .commit-status")
|
||||
assert.Equal(t, 1, sel.Length())
|
||||
for _, class := range classes {
|
||||
assert.True(t, sel.HasClass(class))
|
||||
@ -140,7 +140,7 @@ func TestRepoCommitsStatusParallel(t *testing.T) {
|
||||
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
// Get first commit URL
|
||||
commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href")
|
||||
commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href")
|
||||
assert.True(t, exists)
|
||||
assert.NotEmpty(t, commitURL)
|
||||
|
||||
@ -175,7 +175,7 @@ func TestRepoCommitsStatusMultiple(t *testing.T) {
|
||||
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
// Get first commit URL
|
||||
commitURL, exists := doc.doc.Find("#commits-table tbody tr td.sha a").Attr("href")
|
||||
commitURL, exists := doc.doc.Find("#commits-table .commit-id-short").Attr("href")
|
||||
assert.True(t, exists)
|
||||
assert.NotEmpty(t, commitURL)
|
||||
|
||||
@ -200,6 +200,6 @@ func TestRepoCommitsStatusMultiple(t *testing.T) {
|
||||
|
||||
doc = NewHTMLParser(t, resp.Body)
|
||||
// Check that the data-tippy="commit-statuses" (for trigger) and commit-status (svg) are present
|
||||
sel := doc.doc.Find("#commits-table tbody tr td.message [data-tippy=\"commit-statuses\"] .commit-status")
|
||||
sel := doc.doc.Find("#commits-table .message [data-tippy=\"commit-statuses\"] .commit-status")
|
||||
assert.Equal(t, 1, sel.Length())
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -17,6 +18,7 @@ import (
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testLoginFailed(t *testing.T, username, password, message string) {
|
||||
@ -42,7 +44,7 @@ func TestSignin(t *testing.T) {
|
||||
user.Name = "testuser"
|
||||
user.LowerName = strings.ToLower(user.Name)
|
||||
user.ID = 0
|
||||
unittest.AssertSuccessfulInsert(t, user)
|
||||
require.NoError(t, db.Insert(db.DefaultContext, user))
|
||||
|
||||
samples := []struct {
|
||||
username string
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -99,34 +100,39 @@ func TestSignupEmailActive(t *testing.T) {
|
||||
|
||||
// try to sign up and send the activation email
|
||||
req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{
|
||||
"user_name": "test-user-1",
|
||||
"email": "email-1@example.com",
|
||||
"user_name": "Test-User-1",
|
||||
"email": "EmAiL-1@example.com",
|
||||
"password": "password1",
|
||||
"retype": "password1",
|
||||
})
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), `A new confirmation email has been sent to <b>email-1@example.com</b>.`)
|
||||
assert.Contains(t, resp.Body.String(), `A new confirmation email has been sent to <b>EmAiL-1@example.com</b>.`)
|
||||
|
||||
// access "user/activate" means trying to re-send the activation email
|
||||
session := loginUserWithPassword(t, "test-user-1", "password1")
|
||||
resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/activate"), http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), "You have already requested an activation email recently")
|
||||
|
||||
// access anywhere else will see a "Activate Your Account" prompt, and there is a chance to change email
|
||||
// access anywhere else will see an "Activate Your Account" prompt, and there is a chance to change email
|
||||
resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/issues"), http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), `<input id="change-email" name="change_email" `)
|
||||
|
||||
// post to "user/activate" with a new email
|
||||
session.MakeRequest(t, NewRequestWithValues(t, "POST", "/user/activate", map[string]string{"change_email": "email-changed@example.com"}), http.StatusSeeOther)
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "Test-User-1"})
|
||||
assert.Equal(t, "email-changed@example.com", user.Email)
|
||||
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "email-changed@example.com"})
|
||||
assert.False(t, email.IsActivated)
|
||||
assert.True(t, email.IsPrimary)
|
||||
|
||||
// generate an activation code from lower-cased email
|
||||
activationCode := user_model.GenerateUserTimeLimitCode(&user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeActivateAccount}, user)
|
||||
// and update the user email to case-sensitive, it shouldn't affect the verification later
|
||||
_, _ = db.Exec(db.DefaultContext, "UPDATE `user` SET email=? WHERE id=?", "EmAiL-changed@example.com", user.ID)
|
||||
user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "Test-User-1"})
|
||||
assert.Equal(t, "EmAiL-changed@example.com", user.Email)
|
||||
|
||||
// access "user/activate" with a valid activation code, then get the "verify password" page
|
||||
user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"})
|
||||
activationCode := user.GenerateEmailActivateCode(user.Email)
|
||||
resp = session.MakeRequest(t, NewRequest(t, "GET", "/user/activate?code="+activationCode), http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), `<input id="verify-password"`)
|
||||
|
||||
@ -138,7 +144,7 @@ func TestSignupEmailActive(t *testing.T) {
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), `Your password does not match`)
|
||||
assert.Contains(t, resp.Body.String(), `<input id="verify-password"`)
|
||||
user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"})
|
||||
user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "Test-User-1"})
|
||||
assert.False(t, user.IsActive)
|
||||
|
||||
// then use a correct password, the user should be activated
|
||||
@ -148,6 +154,6 @@ func TestSignupEmailActive(t *testing.T) {
|
||||
})
|
||||
resp = session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
assert.Equal(t, "/", test.RedirectURL(resp))
|
||||
user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "test-user-1"})
|
||||
user = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "Test-User-1"})
|
||||
assert.True(t, user.IsActive)
|
||||
}
|
||||
|
@ -461,7 +461,9 @@ a.label,
|
||||
img.ui.avatar,
|
||||
.ui.avatar img,
|
||||
.ui.avatar svg {
|
||||
border: 1px solid;
|
||||
border-radius: var(--border-radius);
|
||||
border-color: var(--color-secondary);
|
||||
object-fit: contain;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
@ -742,15 +744,10 @@ input:-webkit-autofill:active,
|
||||
font-family: var(--fonts-monospace);
|
||||
font-size: 13px;
|
||||
font-weight: var(--font-weight-normal);
|
||||
margin: 0 6px;
|
||||
padding: 5px 10px;
|
||||
padding: 3px 5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ui .sha.label .shortsha {
|
||||
display: inline-block; /* not sure whether it is still needed */
|
||||
}
|
||||
|
||||
.ui .button.truncate {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
@ -933,7 +930,8 @@ strong.attention-caution, svg.attention-caution {
|
||||
color: var(--color-red-dark-1);
|
||||
}
|
||||
|
||||
.center:not(.popup) {
|
||||
/* FIXME: this is a longstanding dirty patch since 2015, it only makes the pages more messy and shouldn't be used */
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,4 @@
|
||||
.explore .secondary-nav {
|
||||
border-width: 1px !important;
|
||||
}
|
||||
|
||||
.explore .secondary-nav .svg {
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* FIXME: need to refactor the repo branches list page and move these styles to proper place */
|
||||
.ui.repository.branches .info {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-light);
|
||||
@ -20,12 +11,3 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ui.repository.branches .overflow-visible {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* fix alignment of PR popup in branches table */
|
||||
.ui.repository.branches table .ui.popup {
|
||||
text-align: left;
|
||||
}
|
||||
|
@ -57,6 +57,12 @@
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
#git-graph-container li .ui.label.commit-id-short {
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
#git-graph-container li .node-relation {
|
||||
@ -112,17 +118,6 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#git-graph-container #rev-list .sha.label {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
#git-graph-container #rev-list .sha.label .ui.detail.icon.button {
|
||||
padding-top: 3px;
|
||||
margin-top: -5px;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
#git-graph-container #graph-raw-list {
|
||||
margin: 0;
|
||||
}
|
||||
|
@ -82,5 +82,6 @@
|
||||
@import "./review.css";
|
||||
@import "./actions.css";
|
||||
|
||||
@tailwind utilities;
|
||||
@import "./helpers.css";
|
||||
|
||||
@tailwind utilities;
|
||||
|
@ -103,11 +103,11 @@
|
||||
#navbar .ui.dropdown .navbar-profile-admin {
|
||||
display: block;
|
||||
position: absolute;
|
||||
font-size: 10px;
|
||||
font-size: 9px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-nav-bg);
|
||||
background: var(--color-primary);
|
||||
padding: 2px 4px;
|
||||
padding: 2px 3px;
|
||||
border-radius: 10px;
|
||||
top: -1px;
|
||||
left: 18px;
|
||||
|
@ -120,15 +120,13 @@ td .commit-summary {
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
gap: 0.25em;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.latest-commit .sha {
|
||||
.latest-commit .commit-id-short {
|
||||
display: none;
|
||||
}
|
||||
.latest-commit .commit-summary {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.repo-path {
|
||||
@ -605,15 +603,6 @@ td .commit-summary {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.singular-commit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.singular-commit .badge {
|
||||
height: 30px !important;
|
||||
}
|
||||
|
||||
.repository.view.issue .comment-list .timeline-item.event > .commit-status-link {
|
||||
float: right;
|
||||
margin-right: 8px;
|
||||
@ -936,14 +925,6 @@ td .commit-summary {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.repository #commits-table thead .shatd {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.repository #commits-table td.sha .sha.label {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.repository #commits-table.ui.basic.striped.table tbody tr:nth-child(2n) {
|
||||
background-color: var(--color-light) !important;
|
||||
}
|
||||
@ -1440,12 +1421,6 @@ td .commit-summary {
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.commit-header-row {
|
||||
min-height: 50px !important;
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.commit-header-buttons {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
@ -1622,7 +1597,7 @@ td .commit-summary {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.labels-list .label {
|
||||
.labels-list .label, .scope-parent > .label {
|
||||
padding: 0 6px;
|
||||
min-height: 20px;
|
||||
line-height: 1.3; /* there is a `font-size: 1.25em` for inside emoji, so here the line-height needs to be larger slightly */
|
||||
@ -1701,6 +1676,10 @@ tbody.commit-list {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.latest-commit .message-wrapper {
|
||||
max-width: calc(100% - 2.5rem);
|
||||
}
|
||||
|
||||
/* in the commit list, messages can wrap so we can use inline */
|
||||
.commit-list .message-wrapper {
|
||||
display: inline;
|
||||
@ -2128,18 +2107,6 @@ tbody.commit-list {
|
||||
.repository.view.issue .comment-list .timeline .comment-header-right .role-label {
|
||||
display: none;
|
||||
}
|
||||
.commit-header-row .ui.horizontal.list {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.commit-header-row .ui.horizontal.list .item {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
.commit-header-row .author {
|
||||
padding: 3px 0;
|
||||
}
|
||||
.commit-header h3 {
|
||||
flex-basis: auto !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
|
@ -1,272 +1,60 @@
|
||||
|
||||
.repository .ui.attached.isSigned.isWarning {
|
||||
border-left: 1px solid var(--color-error-border);
|
||||
border-right: 1px solid var(--color-error-border);
|
||||
}
|
||||
|
||||
.repository .ui.attached.isSigned.isWarning.top,
|
||||
.repository .ui.attached.isSigned.isWarning.message {
|
||||
border-top: 1px solid var(--color-error-border);
|
||||
}
|
||||
|
||||
.repository .ui.attached.isSigned.isWarning.message {
|
||||
box-shadow: none;
|
||||
background-color: var(--color-error-bg);
|
||||
color: var(--color-error-text);
|
||||
}
|
||||
|
||||
.repository .ui.attached.isSigned.isWarning.message .ui.text {
|
||||
color: var(--color-error-text);
|
||||
}
|
||||
|
||||
.repository .ui.attached.isSigned.isWarning:last-child,
|
||||
.repository .ui.attached.isSigned.isWarning.bottom {
|
||||
border-bottom: 1px solid var(--color-error-border);
|
||||
}
|
||||
|
||||
.repository .ui.attached.isSigned.isVerified {
|
||||
border-left: 1px solid var(--color-success-border);
|
||||
border-right: 1px solid var(--color-success-border);
|
||||
}
|
||||
|
||||
.repository .ui.attached.isSigned.isVerified.top,
|
||||
.repository .ui.attached.isSigned.isVerified.message {
|
||||
border-top: 1px solid var(--color-success-border);
|
||||
}
|
||||
|
||||
.repository .ui.attached.isSigned.isVerified.message {
|
||||
box-shadow: none;
|
||||
background-color: var(--color-success-bg);
|
||||
color: var(--color-success-text);
|
||||
}
|
||||
|
||||
.repository .ui.attached.isSigned.isVerified.message .pull-right {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.repository .ui.attached.isSigned.isVerified.message .ui.text {
|
||||
color: var(--color-success-text);
|
||||
}
|
||||
|
||||
.repository .ui.attached.isSigned.isVerified:last-child,
|
||||
.repository .ui.attached.isSigned.isVerified.bottom {
|
||||
border-bottom: 1px solid var(--color-success-border);
|
||||
}
|
||||
|
||||
.repository .ui.attached.isSigned.isVerifiedUntrusted,
|
||||
.repository .ui.attached.isSigned.isVerifiedUnmatched {
|
||||
border-left: 1px solid var(--color-warning-border);
|
||||
border-right: 1px solid var(--color-warning-border);
|
||||
}
|
||||
|
||||
.repository .ui.attached.isSigned.isVerifiedUntrusted.top,
|
||||
.repository .ui.attached.isSigned.isVerifiedUnmatched.top,
|
||||
.repository .ui.attached.isSigned.isVerifiedUntrusted.message,
|
||||
.repository .ui.attached.isSigned.isVerifiedUnmatched.message {
|
||||
border-top: 1px solid var(--color-warning-border);
|
||||
}
|
||||
|
||||
.repository .ui.attached.isSigned.isVerifiedUntrusted.message,
|
||||
.repository .ui.attached.isSigned.isVerifiedUnmatched.message {
|
||||
box-shadow: none;
|
||||
background-color: var(--color-warning-bg);
|
||||
color: var(--color-warning-text);
|
||||
}
|
||||
|
||||
.repository .ui.attached.isSigned.isVerifiedUntrusted.message .ui.text,
|
||||
.repository .ui.attached.isSigned.isVerifiedUnmatched.message .ui.text {
|
||||
color: var(--color-warning-text);
|
||||
}
|
||||
|
||||
.repository .ui.attached.isSigned.isVerifiedUntrusted:last-child,
|
||||
.repository .ui.attached.isSigned.isVerifiedUnmatched:last-child,
|
||||
.repository .ui.attached.isSigned.isVerifiedUntrusted.bottom,
|
||||
.repository .ui.attached.isSigned.isVerifiedUnmatched.bottom {
|
||||
border-bottom: 1px solid var(--color-warning-border);
|
||||
}
|
||||
|
||||
.repository #commits-table td.sha .sha.label,
|
||||
.repository #repo-files-table .sha.label,
|
||||
.repository #repo-file-commit-box .sha.label,
|
||||
.repository #rev-list .sha.label,
|
||||
.repository .timeline-item.commits-list .singular-commit .sha.label {
|
||||
.ui.label.commit-id-short,
|
||||
.ui.label.commit-sign-badge {
|
||||
border: 1px solid var(--color-light-border);
|
||||
font-size: 13px;
|
||||
font-weight: var(--font-weight-normal);
|
||||
padding: 3px 5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.repository #commits-table td.sha .sha.label .detail.icon,
|
||||
.repository #repo-files-table .sha.label .detail.icon,
|
||||
.repository #repo-file-commit-box .sha.label .detail.icon,
|
||||
.repository #rev-list .sha.label .detail.icon,
|
||||
.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon {
|
||||
background: var(--color-light);
|
||||
margin: -6px -10px -4px 0;
|
||||
padding: 5px 4px 5px 6px;
|
||||
border-left: 1px solid var(--color-light-border);
|
||||
border-top: 0;
|
||||
border-right: 0;
|
||||
border-bottom: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
.ui.label.commit-id-short {
|
||||
font-family: var(--fonts-monospace);
|
||||
}
|
||||
|
||||
.repository #commits-table td.sha .sha.label .detail.icon .svg,
|
||||
.repository #repo-files-table .sha.label .detail.icon .svg,
|
||||
.repository #repo-file-commit-box .sha.label .detail.icon .svg,
|
||||
.repository #rev-list .sha.label .detail.icon .svg,
|
||||
.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon .svg {
|
||||
margin: 0 0.25em 0 0;
|
||||
}
|
||||
|
||||
.repository #commits-table td.sha .sha.label .detail.icon > div,
|
||||
.repository #repo-files-table .sha.label .detail.icon > div,
|
||||
.repository #repo-file-commit-box .sha.label .detail.icon > div,
|
||||
.repository #rev-list .sha.label .detail.icon > div,
|
||||
.repository .timeline-item.commits-list .singular-commit .sha.label .detail.icon > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.repository #commits-table td.sha .sha.label.isSigned.isWarning,
|
||||
.repository #repo-files-table .sha.label.isSigned.isWarning,
|
||||
.repository #repo-file-commit-box .sha.label.isSigned.isWarning,
|
||||
.repository #rev-list .sha.label.isSigned.isWarning,
|
||||
.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isWarning {
|
||||
border: 1px solid var(--color-red-badge);
|
||||
background: var(--color-red-badge-bg);
|
||||
}
|
||||
|
||||
.repository #commits-table td.sha .sha.label.isSigned.isWarning .detail.icon,
|
||||
.repository #repo-files-table .sha.label.isSigned.isWarning .detail.icon,
|
||||
.repository #repo-file-commit-box .sha.label.isSigned.isWarning .detail.icon,
|
||||
.repository #rev-list .sha.label.isSigned.isWarning .detail.icon,
|
||||
.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isWarning .detail.icon {
|
||||
border-left: 1px solid var(--color-red-badge);
|
||||
color: var(--color-red-badge);
|
||||
}
|
||||
|
||||
.repository #commits-table td.sha .sha.label.isSigned.isWarning:hover,
|
||||
.repository #repo-files-table .sha.label.isSigned.isWarning:hover,
|
||||
.repository #repo-file-commit-box .sha.label.isSigned.isWarning:hover,
|
||||
.repository #rev-list .sha.label.isSigned.isWarning:hover,
|
||||
.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isWarning:hover {
|
||||
background: var(--color-red-badge-hover-bg) !important;
|
||||
}
|
||||
|
||||
.repository #commits-table td.sha .sha.label.isSigned.isVerified,
|
||||
.repository #repo-files-table .sha.label.isSigned.isVerified,
|
||||
.repository #repo-file-commit-box .sha.label.isSigned.isVerified,
|
||||
.repository #rev-list .sha.label.isSigned.isVerified,
|
||||
.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerified {
|
||||
border: 1px solid var(--color-green-badge);
|
||||
background: var(--color-green-badge-bg);
|
||||
}
|
||||
|
||||
.repository #commits-table td.sha .sha.label.isSigned.isVerified .detail.icon,
|
||||
.repository #repo-files-table .sha.label.isSigned.isVerified .detail.icon,
|
||||
.repository #repo-file-commit-box .sha.label.isSigned.isVerified .detail.icon,
|
||||
.repository #rev-list .sha.label.isSigned.isVerified .detail.icon,
|
||||
.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerified .detail.icon {
|
||||
border-left: 1px solid var(--color-green-badge);
|
||||
color: var(--color-green-badge);
|
||||
}
|
||||
|
||||
.repository #commits-table td.sha .sha.label.isSigned.isVerified:hover,
|
||||
.repository #repo-files-table .sha.label.isSigned.isVerified:hover,
|
||||
.repository #repo-file-commit-box .sha.label.isSigned.isVerified:hover,
|
||||
.repository #rev-list .sha.label.isSigned.isVerified:hover,
|
||||
.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerified:hover {
|
||||
background: var(--color-green-badge-hover-bg) !important;
|
||||
}
|
||||
|
||||
.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUntrusted,
|
||||
.repository #repo-files-table .sha.label.isSigned.isVerifiedUntrusted,
|
||||
.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUntrusted,
|
||||
.repository #rev-list .sha.label.isSigned.isVerifiedUntrusted,
|
||||
.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUntrusted {
|
||||
border: 1px solid var(--color-yellow-badge);
|
||||
background: var(--color-yellow-badge-bg);
|
||||
}
|
||||
|
||||
.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUntrusted .detail.icon,
|
||||
.repository #repo-files-table .sha.label.isSigned.isVerifiedUntrusted .detail.icon,
|
||||
.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUntrusted .detail.icon,
|
||||
.repository #rev-list .sha.label.isSigned.isVerifiedUntrusted .detail.icon,
|
||||
.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUntrusted .detail.icon {
|
||||
border-left: 1px solid var(--color-yellow-badge);
|
||||
color: var(--color-yellow-badge);
|
||||
}
|
||||
|
||||
.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUntrusted:hover,
|
||||
.repository #repo-files-table .sha.label.isSigned.isVerifiedUntrusted:hover,
|
||||
.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUntrusted:hover,
|
||||
.repository #rev-list .sha.label.isSigned.isVerifiedUntrusted:hover,
|
||||
.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUntrusted:hover {
|
||||
background: var(--color-yellow-badge-hover-bg) !important;
|
||||
}
|
||||
|
||||
.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUnmatched,
|
||||
.repository #repo-files-table .sha.label.isSigned.isVerifiedUnmatched,
|
||||
.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUnmatched,
|
||||
.repository #rev-list .sha.label.isSigned.isVerifiedUnmatched,
|
||||
.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUnmatched {
|
||||
border: 1px solid var(--color-orange-badge);
|
||||
background: var(--color-orange-badge-bg);
|
||||
}
|
||||
|
||||
.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUnmatched .detail.icon,
|
||||
.repository #repo-files-table .sha.label.isSigned.isVerifiedUnmatched .detail.icon,
|
||||
.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUnmatched .detail.icon,
|
||||
.repository #rev-list .sha.label.isSigned.isVerifiedUnmatched .detail.icon,
|
||||
.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUnmatched .detail.icon {
|
||||
border-left: 1px solid var(--color-orange-badge);
|
||||
color: var(--color-orange-badge);
|
||||
}
|
||||
|
||||
.repository #commits-table td.sha .sha.label.isSigned.isVerifiedUnmatched:hover,
|
||||
.repository #repo-files-table .sha.label.isSigned.isVerifiedUnmatched:hover,
|
||||
.repository #repo-file-commit-box .sha.label.isSigned.isVerifiedUnmatched:hover,
|
||||
.repository #rev-list .sha.label.isSigned.isVerifiedUnmatched:hover,
|
||||
.repository .timeline-item.commits-list .singular-commit .sha.label.isSigned.isVerifiedUnmatched:hover {
|
||||
background: var(--color-orange-badge-hover-bg) !important;
|
||||
}
|
||||
|
||||
.singular-commit .shabox .sha.label {
|
||||
.ui.label.commit-id-short > .commit-sign-badge {
|
||||
margin: 0;
|
||||
border: 1px solid var(--color-light-border);
|
||||
padding: 0;
|
||||
border: 0 !important;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.singular-commit .shabox .sha.label.isSigned.isWarning {
|
||||
border: 1px solid var(--color-red-badge);
|
||||
background: var(--color-red-badge-bg);
|
||||
.ui.label.commit-id-short > .commit-sign-badge:hover {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.singular-commit .shabox .sha.label.isSigned.isWarning:hover {
|
||||
background: var(--color-red-badge-hover-bg) !important;
|
||||
.commit-is-signed.sign-trusted {
|
||||
border: 1px solid var(--color-green-badge) !important;
|
||||
background: var(--color-green-badge-bg) !important;
|
||||
}
|
||||
|
||||
.singular-commit .shabox .sha.label.isSigned.isVerified {
|
||||
border: 1px solid var(--color-green-badge);
|
||||
background: var(--color-green-badge-bg);
|
||||
}
|
||||
|
||||
.singular-commit .shabox .sha.label.isSigned.isVerified:hover {
|
||||
.commit-is-signed.sign-trusted:hover {
|
||||
background: var(--color-green-badge-hover-bg) !important;
|
||||
}
|
||||
|
||||
.singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted {
|
||||
border: 1px solid var(--color-yellow-badge);
|
||||
background: var(--color-yellow-badge-bg);
|
||||
.commit-is-signed.sign-untrusted {
|
||||
border: 1px solid var(--color-yellow-badge) !important;
|
||||
background: var(--color-yellow-badge-bg) !important;
|
||||
}
|
||||
|
||||
.singular-commit .shabox .sha.label.isSigned.isVerifiedUntrusted:hover {
|
||||
.commit-is-signed.sign-untrusted:hover {
|
||||
background: var(--color-yellow-badge-hover-bg) !important;
|
||||
}
|
||||
|
||||
.singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched {
|
||||
border: 1px solid var(--color-orange-badge);
|
||||
background: var(--color-orange-badge-bg);
|
||||
.commit-is-signed.sign-unmatched {
|
||||
border: 1px solid var(--color-orange-badge) !important;
|
||||
background: var(--color-orange-badge-bg) !important;
|
||||
}
|
||||
|
||||
.singular-commit .shabox .sha.label.isSigned.isVerifiedUnmatched:hover {
|
||||
.commit-is-signed.sign-unmatched:hover {
|
||||
background: var(--color-orange-badge-hover-bg) !important;
|
||||
}
|
||||
|
||||
.commit-is-signed.sign-warning {
|
||||
border: 1px solid var(--color-red-badge) !important;
|
||||
background: var(--color-red-badge-bg) !important;
|
||||
}
|
||||
|
||||
.commit-is-signed.sign-warning:hover {
|
||||
background: var(--color-red-badge-hover-bg) !important;
|
||||
}
|
||||
|
@ -38,6 +38,8 @@ function initRepoDiffFileViewToggle() {
|
||||
}
|
||||
|
||||
function initRepoDiffConversationForm() {
|
||||
// FIXME: there could be various different form in a conversation-holder (for example: reply form, edit form).
|
||||
// This listener is for "reply form" only, it should clearly distinguish different forms in the future.
|
||||
addDelegatedEventListener<HTMLFormElement, SubmitEvent>(document, 'submit', '.conversation-holder form', async (form, e) => {
|
||||
e.preventDefault();
|
||||
const textArea = form.querySelector<HTMLTextAreaElement>('textarea');
|
||||
|
@ -1,20 +1,17 @@
|
||||
import $ from 'jquery';
|
||||
import {svg} from '../svg.ts';
|
||||
import {showErrorToast} from '../modules/toast.ts';
|
||||
import {GET, POST} from '../modules/fetch.ts';
|
||||
import {showElem} from '../utils/dom.ts';
|
||||
import {createElementFromHTML, showElem} from '../utils/dom.ts';
|
||||
import {parseIssuePageInfo} from '../utils.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
|
||||
let i18nTextEdited;
|
||||
let i18nTextOptions;
|
||||
let i18nTextDeleteFromHistory;
|
||||
let i18nTextDeleteFromHistoryConfirm;
|
||||
let i18nTextEdited: string;
|
||||
let i18nTextOptions: string;
|
||||
let i18nTextDeleteFromHistory: string;
|
||||
let i18nTextDeleteFromHistoryConfirm: string;
|
||||
|
||||
function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleHtml) {
|
||||
let $dialog = $('.content-history-detail-dialog');
|
||||
if ($dialog.length) return;
|
||||
|
||||
$dialog = $(`
|
||||
function showContentHistoryDetail(issueBaseUrl: string, commentId: string, historyId: string, itemTitleHtml: string) {
|
||||
const elDetailDialog = createElementFromHTML(`
|
||||
<div class="ui modal content-history-detail-dialog">
|
||||
${svg('octicon-x', 16, 'close icon inside')}
|
||||
<div class="header tw-flex tw-items-center tw-justify-between">
|
||||
@ -29,8 +26,11 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH
|
||||
</div>
|
||||
<div class="comment-diff-data is-loading"></div>
|
||||
</div>`);
|
||||
$dialog.appendTo($('body'));
|
||||
$dialog.find('.dialog-header-options').dropdown({
|
||||
document.body.append(elDetailDialog);
|
||||
const elOptionsDropdown = elDetailDialog.querySelector('.ui.dropdown.dialog-header-options');
|
||||
const $fomanticDialog = fomanticQuery(elDetailDialog);
|
||||
const $fomanticDropdownOptions = fomanticQuery(elOptionsDropdown);
|
||||
$fomanticDropdownOptions.dropdown({
|
||||
showOnFocus: false,
|
||||
allowReselection: true,
|
||||
async onChange(_value, _text, $item) {
|
||||
@ -46,7 +46,7 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH
|
||||
const resp = await response.json();
|
||||
|
||||
if (resp.ok) {
|
||||
$dialog.modal('hide');
|
||||
$fomanticDialog.modal('hide');
|
||||
} else {
|
||||
showErrorToast(resp.message);
|
||||
}
|
||||
@ -60,10 +60,10 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH
|
||||
}
|
||||
},
|
||||
onHide() {
|
||||
$(this).dropdown('clear', true);
|
||||
$fomanticDropdownOptions.dropdown('clear', true);
|
||||
},
|
||||
});
|
||||
$dialog.modal({
|
||||
$fomanticDialog.modal({
|
||||
async onShow() {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
@ -74,25 +74,25 @@ function showContentHistoryDetail(issueBaseUrl, commentId, historyId, itemTitleH
|
||||
const response = await GET(url);
|
||||
const resp = await response.json();
|
||||
|
||||
const commentDiffData = $dialog.find('.comment-diff-data')[0];
|
||||
commentDiffData?.classList.remove('is-loading');
|
||||
const commentDiffData = elDetailDialog.querySelector('.comment-diff-data');
|
||||
commentDiffData.classList.remove('is-loading');
|
||||
commentDiffData.innerHTML = resp.diffHtml;
|
||||
// there is only one option "item[data-option-item=delete]", so the dropdown can be entirely shown/hidden.
|
||||
if (resp.canSoftDelete) {
|
||||
showElem($dialog.find('.dialog-header-options'));
|
||||
showElem(elOptionsDropdown);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
},
|
||||
onHidden() {
|
||||
$dialog.remove();
|
||||
$fomanticDialog.remove();
|
||||
},
|
||||
}).modal('show');
|
||||
}
|
||||
|
||||
function showContentHistoryMenu(issueBaseUrl, $item, commentId) {
|
||||
const $headerLeft = $item.find('.comment-header-left');
|
||||
function showContentHistoryMenu(issueBaseUrl: string, elCommentItem: Element, commentId: string) {
|
||||
const elHeaderLeft = elCommentItem.querySelector('.comment-header-left');
|
||||
const menuHtml = `
|
||||
<div class="ui dropdown interact-fg content-history-menu" data-comment-id="${commentId}">
|
||||
• ${i18nTextEdited}${svg('octicon-triangle-down', 14, 'dropdown icon')}
|
||||
@ -100,9 +100,12 @@ function showContentHistoryMenu(issueBaseUrl, $item, commentId) {
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
$headerLeft.find(`.content-history-menu`).remove();
|
||||
$headerLeft.append($(menuHtml));
|
||||
$headerLeft.find('.dropdown').dropdown({
|
||||
elHeaderLeft.querySelector(`.ui.dropdown.content-history-menu`)?.remove(); // remove the old one if exists
|
||||
elHeaderLeft.append(createElementFromHTML(menuHtml));
|
||||
|
||||
const elDropdown = elHeaderLeft.querySelector('.ui.dropdown.content-history-menu');
|
||||
const $fomanticDropdown = fomanticQuery(elDropdown);
|
||||
$fomanticDropdown.dropdown({
|
||||
action: 'hide',
|
||||
apiSettings: {
|
||||
cache: false,
|
||||
@ -110,7 +113,7 @@ function showContentHistoryMenu(issueBaseUrl, $item, commentId) {
|
||||
},
|
||||
saveRemoteData: false,
|
||||
onHide() {
|
||||
$(this).dropdown('change values', null);
|
||||
$fomanticDropdown.dropdown('change values', null);
|
||||
},
|
||||
onChange(value, itemHtml, $item) {
|
||||
if (value && !$item.find('[data-history-is-deleted=1]').length) {
|
||||
@ -124,9 +127,9 @@ export async function initRepoIssueContentHistory() {
|
||||
const issuePageInfo = parseIssuePageInfo();
|
||||
if (!issuePageInfo.issueNumber) return;
|
||||
|
||||
const $itemIssue = $('.repository.issue .timeline-item.comment.first'); // issue(PR) main content
|
||||
const $comments = $('.repository.issue .comment-list .comment'); // includes: issue(PR) comments, review comments, code comments
|
||||
if (!$itemIssue.length && !$comments.length) return;
|
||||
const elIssueDescription = document.querySelector('.repository.issue .timeline-item.comment.first'); // issue(PR) main content
|
||||
const elComments = document.querySelectorAll('.repository.issue .comment-list .comment'); // includes: issue(PR) comments, review comments, code comments
|
||||
if (!elIssueDescription && !elComments.length) return;
|
||||
|
||||
const issueBaseUrl = `${issuePageInfo.repoLink}/issues/${issuePageInfo.issueNumber}`;
|
||||
|
||||
@ -139,13 +142,13 @@ export async function initRepoIssueContentHistory() {
|
||||
i18nTextDeleteFromHistoryConfirm = resp.i18n.textDeleteFromHistoryConfirm;
|
||||
i18nTextOptions = resp.i18n.textOptions;
|
||||
|
||||
if (resp.editedHistoryCountMap[0] && $itemIssue.length) {
|
||||
showContentHistoryMenu(issueBaseUrl, $itemIssue, '0');
|
||||
if (resp.editedHistoryCountMap[0] && elIssueDescription) {
|
||||
showContentHistoryMenu(issueBaseUrl, elIssueDescription, '0');
|
||||
}
|
||||
for (const [commentId, _editedCount] of Object.entries(resp.editedHistoryCountMap)) {
|
||||
if (commentId === '0') continue;
|
||||
const $itemComment = $(`#issuecomment-${commentId}`);
|
||||
showContentHistoryMenu(issueBaseUrl, $itemComment, commentId);
|
||||
const elIssueComment = document.querySelector(`#issuecomment-${commentId}`);
|
||||
if (elIssueComment) showContentHistoryMenu(issueBaseUrl, elIssueComment, commentId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
|
@ -30,6 +30,9 @@ async function tryOnEditContent(e) {
|
||||
|
||||
const saveAndRefresh = async (e) => {
|
||||
e.preventDefault();
|
||||
// we are already in a form, do not bubble up to the document otherwise there will be other "form submit handlers"
|
||||
// at the moment, the form submit event conflicts with initRepoDiffConversationForm (global '.conversation-holder form' event handler)
|
||||
e.stopPropagation();
|
||||
renderContent.classList.add('is-loading');
|
||||
showElem(renderContent);
|
||||
hideElem(editContentZone);
|
||||
|
Loading…
Reference in New Issue
Block a user