Compare commits

...

21 Commits

Author SHA1 Message Date
Henrique Pimentel 0f0b5ef7be
Merge 52957f4406 into 29b28002aa 2025-06-23 09:58:20 +08:00
GiteaBot 29b28002aa [skip ci] Updated translations via Crowdin 2025-06-23 00:40:49 +00:00
Zettat123 618e2d8106
Fix required contexts and commit status matching bug (#34815)
Fix #34504

Since one required context can match more than one commit statuses, we
should not directly compare the lengths of `requiredCommitStatuses` and
`requiredContexts`

---------

Signed-off-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-06-22 23:31:46 +00:00
NorthRealm 485d8f1121
Add "Cancel workflow run" button to Actions list page (#34817) 2025-06-22 19:05:16 -04:00
Kilisei 181db69e0c
Use `shallowRef` instead of `ref` in `.vue` files where possible (#34813)
This PR improves some `.vue` components by using `shallowRef instead of
ref`, which `should improve performance`. It's probably not significant,
but it's an improvement because Vue no longer deep watches the ref
(shallowRef). Also i used `useTemplateRef` instead of `ref`.

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-06-22 21:37:03 +08:00
Brecht Van Lommel a46b16f10f
Edit file workflow for creating a fork and proposing changes (#34240)
When viewing a file that the user can't edit because they can't write to
the branch, the new, upload, patch, edit and delete functionality is no
longer disabled.

If no user fork of the repository exists, there is now a page to create one.
It will automatically create a fork with a single branch matching the one
being viewed, and a unique repository name will be automatically picked.

When a fork exists, but it's archived, a mirror or the user can't write
code to it, there will instead be a message explaining the situation.

If the usable fork exists, a message will appear at the top of the edit page
explaining that the changes will be applied to a branch in the fork. The
base repository branch will be pushed to a new branch to the fork, and
then the edits will be applied on top.

The suggestion to fork happens when accessing /_edit/, so that for
example online documentation can have an "edit this page" link to
the base repository that does the right thing.

Also includes changes to properly report errors when trying to commit
to a new branch that is protected, and when trying to commit to an
existing branch when choosing the new branch option.

Resolves #9017, #20882

---------

Co-authored-by: Brecht Van Lommel <brecht@blender.org>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2025-06-22 12:43:43 +00:00
techknowlogick 52957f4406
Merge branch 'main' into Badge 2025-04-17 18:42:44 -04:00
techknowlogick 4890a15467
Merge branch 'main' into Badge 2025-01-22 17:01:14 -05:00
techknowlogick f5c6a31fc2 fix lint 2025-01-15 22:56:05 -05:00
techknowlogick 789b73bd55 update per feedback 2025-01-15 22:44:38 -05:00
techknowlogick f601501fc1 Update per feedback 2025-01-15 14:21:40 -05:00
techknowlogick 9234ef5ecc
Merge branch 'main' into Badge 2025-01-15 13:20:29 -05:00
techknowlogick ae9c0a6e62
Merge branch 'main' into Badge 2024-10-12 22:23:41 -04:00
techknowlogick d9efbf30ca
close session immediately 2024-10-08 12:29:47 -04:00
techknowlogick d55e25ff4d
update join 2024-10-08 12:26:39 -04:00
techknowlogick 401d2572f9
Merge branch 'main' into Badge 2024-10-08 12:25:04 -04:00
techknowlogick 52bc49dc8c fix lints 2024-09-07 22:29:09 -04:00
techknowlogick 10463ec271
Merge branch 'main' into Badge 2024-09-07 18:09:07 -04:00
Henrique Pimentel d681313609 Fix linting recommendations 2024-06-19 13:53:18 +01:00
Henrique Pimentel f68e44daed Implemented User Badge Management Interface (#29798)
Co-authored-by: Diogo Vicente <diogo.m.s.vicente@tecnico.ulisboa.pt>
2024-06-19 13:53:18 +01:00
Henrique Pimentel 8b86c3140a Implemented Badge Management in administration panel (#29798)
Co-authored-by: Diogo Vicente <diogo.m.s.vicente@tecnico.ulisboa.pt>
2024-06-19 13:53:18 +01:00
83 changed files with 1981 additions and 503 deletions

View File

@ -653,7 +653,7 @@ func (repo *Repository) AllowsPulls(ctx context.Context) bool {
// CanEnableEditor returns true if repository meets the requirements of web editor. // CanEnableEditor returns true if repository meets the requirements of web editor.
func (repo *Repository) CanEnableEditor() bool { func (repo *Repository) CanEnableEditor() bool {
return !repo.IsMirror return !repo.IsMirror && !repo.IsArchived
} }
// DescriptionHTML does special handles to description and return HTML string. // DescriptionHTML does special handles to description and return HTML string.

View File

@ -6,8 +6,12 @@ package user
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
) )
// Badge represents a user badge // Badge represents a user badge
@ -25,6 +29,50 @@ type UserBadge struct { //nolint:revive
UserID int64 `xorm:"INDEX"` UserID int64 `xorm:"INDEX"`
} }
// ErrBadgeAlreadyExist represents a "badge already exists" error.
type ErrBadgeAlreadyExist struct {
Slug string
}
// IsErrBadgeAlreadyExist checks if an error is a ErrBadgeAlreadyExist.
func IsErrBadgeAlreadyExist(err error) bool {
_, ok := err.(ErrBadgeAlreadyExist)
return ok
}
func (err ErrBadgeAlreadyExist) Error() string {
return fmt.Sprintf("badge already exists [slug: %s]", err.Slug)
}
// Unwrap unwraps this error as a ErrExist error
func (err ErrBadgeAlreadyExist) Unwrap() error {
return util.ErrAlreadyExist
}
// ErrBadgeNotExist represents a "BadgeNotExist" kind of error.
type ErrBadgeNotExist struct {
Slug string
ID int64
}
func (err ErrBadgeNotExist) Error() string {
if err.ID > 0 {
return fmt.Sprintf("badge does not exist [id: %d]", err.ID)
}
return fmt.Sprintf("badge does not exist [slug: %s]", err.Slug)
}
// IsErrBadgeNotExist checks if an error is a ErrBadgeNotExist.
func IsErrBadgeNotExist(err error) bool {
_, ok := err.(ErrBadgeNotExist)
return ok
}
// Unwrap unwraps this error as a ErrNotExist error
func (err ErrBadgeNotExist) Unwrap() error {
return util.ErrNotExist
}
func init() { func init() {
db.RegisterModel(new(Badge)) db.RegisterModel(new(Badge))
db.RegisterModel(new(UserBadge)) db.RegisterModel(new(UserBadge))
@ -42,13 +90,37 @@ func GetUserBadges(ctx context.Context, u *User) ([]*Badge, int64, error) {
return badges, count, err return badges, count, err
} }
// GetBadgeUsersOptions contains options for getting users with a specific badge
type GetBadgeUsersOptions struct {
db.ListOptions
Badge *Badge
}
// GetBadgeUsers returns the users that have a specific badge with pagination support.
func GetBadgeUsers(ctx context.Context, opts *GetBadgeUsersOptions) ([]*User, int64, error) {
sess := db.GetEngine(ctx).
Select("`user`.*").
Join("INNER", "user_badge", "`user_badge`.user_id=user.id").
Join("INNER", "badge", "`user_badge`.badge_id=badge.id").
Where("badge.slug=?", opts.Badge.Slug)
if opts.Page > 0 {
sess = db.SetSessionPagination(sess, opts)
}
users := make([]*User, 0, opts.PageSize)
count, err := sess.FindAndCount(&users)
return users, count, err
}
// CreateBadge creates a new badge. // CreateBadge creates a new badge.
func CreateBadge(ctx context.Context, badge *Badge) error { func CreateBadge(ctx context.Context, badge *Badge) error {
// this will fail if the badge already exists due to the UNIQUE constraint
_, err := db.GetEngine(ctx).Insert(badge) _, err := db.GetEngine(ctx).Insert(badge)
return err return err
} }
// GetBadge returns a badge // GetBadge returns a specific badge
func GetBadge(ctx context.Context, slug string) (*Badge, error) { func GetBadge(ctx context.Context, slug string) (*Badge, error) {
badge := new(Badge) badge := new(Badge)
has, err := db.GetEngine(ctx).Where("slug=?", slug).Get(badge) has, err := db.GetEngine(ctx).Where("slug=?", slug).Get(badge)
@ -60,14 +132,26 @@ func GetBadge(ctx context.Context, slug string) (*Badge, error) {
// UpdateBadge updates a badge based on its slug. // UpdateBadge updates a badge based on its slug.
func UpdateBadge(ctx context.Context, badge *Badge) error { func UpdateBadge(ctx context.Context, badge *Badge) error {
_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Update(badge) _, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Cols("description", "image_url").Update(badge)
return err return err
} }
// DeleteBadge deletes a badge. // DeleteBadge deletes a badge and all associated user_badge entries.
func DeleteBadge(ctx context.Context, badge *Badge) error { func DeleteBadge(ctx context.Context, badge *Badge) error {
_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Delete(badge) return db.WithTx(ctx, func(ctx context.Context) error {
return err // First delete all user_badge entries for this badge
if _, err := db.GetEngine(ctx).
Where("badge_id = (SELECT id FROM badge WHERE slug = ?)", badge.Slug).
Delete(&UserBadge{}); err != nil {
return err
}
// Then delete the badge itself
if _, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Delete(badge); err != nil {
return err
}
return nil
})
} }
// AddUserBadge adds a badge to a user. // AddUserBadge adds a badge to a user.
@ -84,7 +168,7 @@ func AddUserBadges(ctx context.Context, u *User, badges []*Badge) error {
if err != nil { if err != nil {
return err return err
} else if !has { } else if !has {
return fmt.Errorf("badge with slug %s doesn't exist", badge.Slug) return ErrBadgeNotExist{Slug: badge.Slug}
} }
if err := db.Insert(ctx, &UserBadge{ if err := db.Insert(ctx, &UserBadge{
BadgeID: badge.ID, BadgeID: badge.ID,
@ -102,16 +186,24 @@ func RemoveUserBadge(ctx context.Context, u *User, badge *Badge) error {
return RemoveUserBadges(ctx, u, []*Badge{badge}) return RemoveUserBadges(ctx, u, []*Badge{badge})
} }
// RemoveUserBadges removes badges from a user. // RemoveUserBadges removes specific badges from a user.
func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error { func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error {
return db.WithTx(ctx, func(ctx context.Context) error { return db.WithTx(ctx, func(ctx context.Context) error {
for _, badge := range badges { slugs := make([]string, len(badges))
if _, err := db.GetEngine(ctx). for i, badge := range badges {
Join("INNER", "badge", "badge.id = `user_badge`.badge_id"). slugs[i] = badge.Slug
Where("`user_badge`.user_id=? AND `badge`.slug=?", u.ID, badge.Slug). }
Delete(&UserBadge{}); err != nil {
return err var badgeIDs []int64
} if err := db.GetEngine(ctx).Table("badge").In("slug", slugs).Cols("id").Find(&badgeIDs); err != nil {
return err
}
if _, err := db.GetEngine(ctx).
Where("user_id = ?", u.ID).
In("badge_id", badgeIDs).
Delete(&UserBadge{}); err != nil {
return err
} }
return nil return nil
}) })
@ -122,3 +214,56 @@ func RemoveAllUserBadges(ctx context.Context, u *User) error {
_, err := db.GetEngine(ctx).Where("user_id=?", u.ID).Delete(&UserBadge{}) _, err := db.GetEngine(ctx).Where("user_id=?", u.ID).Delete(&UserBadge{})
return err return err
} }
// SearchBadgeOptions represents the options when fdin badges
type SearchBadgeOptions struct {
db.ListOptions
Keyword string
Slug string
ID int64
OrderBy db.SearchOrderBy
Actor *User // The user doing the search
}
func (opts *SearchBadgeOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.Keyword != "" {
lowerKeyword := strings.ToLower(opts.Keyword)
keywordCond := builder.Or(
builder.Like{"badge.slug", lowerKeyword},
builder.Like{"badge.description", lowerKeyword},
builder.Like{"badge.id", lowerKeyword},
)
cond = cond.And(keywordCond)
}
if opts.ID > 0 {
cond = cond.And(builder.Eq{"badge.id": opts.ID})
}
if len(opts.Slug) > 0 {
cond = cond.And(builder.Eq{"badge.slug": opts.Slug})
}
return cond
}
// SearchBadges returns badges based on the provided SearchBadgeOptions options
func SearchBadges(ctx context.Context, opts *SearchBadgeOptions) ([]*Badge, int64, error) {
return db.FindAndCount[Badge](ctx, opts)
}
// GetBadgeByID returns a specific badge by ID
func GetBadgeByID(ctx context.Context, id int64) (*Badge, error) {
badge := new(Badge)
has, err := db.GetEngine(ctx).ID(id).Get(badge)
if err != nil {
return nil, err
}
if !has {
return nil, ErrBadgeNotExist{ID: id}
}
return badge, nil
}

61
models/user/badge_test.go Normal file
View File

@ -0,0 +1,61 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user_test
import (
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"github.com/stretchr/testify/assert"
)
func TestGetBadgeUsers(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
// Create a test badge
badge := &user_model.Badge{
Slug: "test-badge",
Description: "Test Badge",
ImageURL: "test.png",
}
assert.NoError(t, user_model.CreateBadge(db.DefaultContext, badge))
// Create test users and assign badges
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
assert.NoError(t, user_model.AddUserBadge(db.DefaultContext, user1, badge))
assert.NoError(t, user_model.AddUserBadge(db.DefaultContext, user2, badge))
// Test getting users with pagination
opts := &user_model.GetBadgeUsersOptions{
Badge: badge,
ListOptions: db.ListOptions{
Page: 1,
PageSize: 1,
},
}
users, count, err := user_model.GetBadgeUsers(db.DefaultContext, opts)
assert.NoError(t, err)
assert.EqualValues(t, 2, count)
assert.Len(t, users, 1)
// Test second page
opts.Page = 2
users, count, err = user_model.GetBadgeUsers(db.DefaultContext, opts)
assert.NoError(t, err)
assert.EqualValues(t, 2, count)
assert.Len(t, users, 1)
// Test with non-existent badge
opts.Badge = &user_model.Badge{Slug: "non-existent"}
users, count, err = user_model.GetBadgeUsers(db.DefaultContext, opts)
assert.NoError(t, err)
assert.EqualValues(t, 0, count)
assert.Empty(t, users)
}

View File

@ -27,6 +27,8 @@ const (
ErrUsername = "UsernameError" ErrUsername = "UsernameError"
// ErrInvalidGroupTeamMap is returned when a group team mapping is invalid // ErrInvalidGroupTeamMap is returned when a group team mapping is invalid
ErrInvalidGroupTeamMap = "InvalidGroupTeamMap" ErrInvalidGroupTeamMap = "InvalidGroupTeamMap"
// ErrInvalidSlug is returned when a slug is invalid
ErrInvalidSlug = "InvalidSlug"
) )
// AddBindingRules adds additional binding rules // AddBindingRules adds additional binding rules
@ -40,6 +42,7 @@ func AddBindingRules() {
addGlobOrRegexPatternRule() addGlobOrRegexPatternRule()
addUsernamePatternRule() addUsernamePatternRule()
addValidGroupTeamMapRule() addValidGroupTeamMapRule()
addSlugPatternRule()
} }
func addGitRefNameBindingRule() { func addGitRefNameBindingRule() {
@ -123,6 +126,22 @@ func addValidSiteURLBindingRule() {
}) })
} }
func addSlugPatternRule() {
binding.AddRule(&binding.Rule{
IsMatch: func(rule string) bool {
return rule == "Slug"
},
IsValid: func(errs binding.Errors, name string, val any) (bool, binding.Errors) {
str := fmt.Sprintf("%v", val)
if !IsValidSlug(str) {
errs.Add([]string{name}, ErrInvalidSlug, "invalid slug")
return false, errs
}
return true, errs
},
})
}
func addGlobPatternRule() { func addGlobPatternRule() {
binding.AddRule(&binding.Rule{ binding.AddRule(&binding.Rule{
IsMatch: func(rule string) bool { IsMatch: func(rule string) bool {

View File

@ -132,3 +132,7 @@ func IsValidUsername(name string) bool {
vars := globalVars() vars := globalVars()
return vars.validUsernamePattern.MatchString(name) && !vars.invalidUsernamePattern.MatchString(name) return vars.validUsernamePattern.MatchString(name) && !vars.invalidUsernamePattern.MatchString(name)
} }
func IsValidSlug(slug string) bool {
return IsValidUsername(slug)
}

View File

@ -138,6 +138,8 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
data["ErrorMsg"] = trName + l.TrString("form.username_error") data["ErrorMsg"] = trName + l.TrString("form.username_error")
case validation.ErrInvalidGroupTeamMap: case validation.ErrInvalidGroupTeamMap:
data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message) data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message)
case validation.ErrInvalidSlug:
data["ErrorMsg"] = l.TrString("form.invalid_slug_error")
default: default:
msg := errs[0].Classification msg := errs[0].Classification
if msg != "" && errs[0].Message != "" { if msg != "" && errs[0].Message != "" {

View File

@ -1368,6 +1368,7 @@ editor.require_signed_commit=Větev vyžaduje podepsaný commit
editor.cherry_pick=Cherry-pick %s na: editor.cherry_pick=Cherry-pick %s na:
editor.revert=Vrátit %s na: editor.revert=Vrátit %s na:
commits.desc=Procházet historii změn zdrojového kódu. commits.desc=Procházet historii změn zdrojového kódu.
commits.commits=Commity commits.commits=Commity
commits.no_commits=Žádné společné commity. „%s“ a „%s“ mají zcela odlišnou historii. commits.no_commits=Žádné společné commity. „%s“ a „%s“ mají zcela odlišnou historii.

View File

@ -1392,6 +1392,7 @@ editor.require_signed_commit=Branch erfordert einen signierten Commit
editor.cherry_pick=Cherry-Picke %s von: editor.cherry_pick=Cherry-Picke %s von:
editor.revert=%s zurücksetzen auf: editor.revert=%s zurücksetzen auf:
commits.desc=Durchsuche die Quellcode-Änderungshistorie. commits.desc=Durchsuche die Quellcode-Änderungshistorie.
commits.commits=Commits commits.commits=Commits
commits.no_commits=Keine gemeinsamen Commits. "%s" und "%s" haben vollständig unterschiedliche Historien. commits.no_commits=Keine gemeinsamen Commits. "%s" und "%s" haben vollständig unterschiedliche Historien.

View File

@ -1226,6 +1226,7 @@ editor.require_signed_commit=Ο κλάδος απαιτεί υπογεγραμμ
editor.cherry_pick=Ανθολόγηση (cherry-pic) του %s στο: editor.cherry_pick=Ανθολόγηση (cherry-pic) του %s στο:
editor.revert=Απόσυρση του %s στο: editor.revert=Απόσυρση του %s στο:
commits.desc=Δείτε το ιστορικό αλλαγών του πηγαίου κώδικα. commits.desc=Δείτε το ιστορικό αλλαγών του πηγαίου κώδικα.
commits.commits=Υποβολές commits.commits=Υποβολές
commits.no_commits=Δεν υπάρχουν κοινές υποβολές. Τα "%s" και "%s" έχουν εντελώς διαφορετικές ιστορίες. commits.no_commits=Δεν υπάρχουν κοινές υποβολές. Τα "%s" και "%s" έχουν εντελώς διαφορετικές ιστορίες.

View File

@ -180,6 +180,7 @@ exact = Exact
exact_tooltip = Include only results that match the exact search term exact_tooltip = Include only results that match the exact search term
repo_kind = Search repos... repo_kind = Search repos...
user_kind = Search users... user_kind = Search users...
badge_kind = Search badges...
org_kind = Search orgs... org_kind = Search orgs...
team_kind = Search teams... team_kind = Search teams...
code_kind = Search code... code_kind = Search code...
@ -574,6 +575,7 @@ PayloadUrl = Payload URL
TeamName = Team name TeamName = Team name
AuthName = Authorization name AuthName = Authorization name
AdminEmail = Admin email AdminEmail = Admin email
ImageURL = Image URL
NewBranchName = New branch name NewBranchName = New branch name
CommitSummary = Commit summary CommitSummary = Commit summary
@ -603,12 +605,15 @@ unknown_error = Unknown error:
captcha_incorrect = The CAPTCHA code is incorrect. captcha_incorrect = The CAPTCHA code is incorrect.
password_not_match = The passwords do not match. password_not_match = The passwords do not match.
lang_select_error = Select a language from the list. lang_select_error = Select a language from the list.
invalid_image_url_error = `Please provide a valid image URL.`
invalid_slug_error = `Please provide a valid slug.`
username_been_taken = The username is already taken. username_been_taken = The username is already taken.
username_change_not_local_user = Non-local users are not allowed to change their username. username_change_not_local_user = Non-local users are not allowed to change their username.
change_username_disabled = Changing username is disabled. change_username_disabled = Changing username is disabled.
change_full_name_disabled = Changing full name is disabled. change_full_name_disabled = Changing full name is disabled.
username_has_not_been_changed = Username has not been changed username_has_not_been_changed = Username has not been changed
slug_been_taken = The slug is already taken.
repo_name_been_taken = The repository name is already used. repo_name_been_taken = The repository name is already used.
repository_force_private = Force Private is enabled: private repositories cannot be made public. repository_force_private = Force Private is enabled: private repositories cannot be made public.
repository_files_already_exist = Files already exist for this repository. Contact the system administrator. repository_files_already_exist = Files already exist for this repository. Contact the system administrator.
@ -1399,6 +1404,13 @@ editor.revert = Revert %s onto:
editor.failed_to_commit = Failed to commit changes. editor.failed_to_commit = Failed to commit changes.
editor.failed_to_commit_summary = Error Message: editor.failed_to_commit_summary = Error Message:
editor.fork_create = Fork Repository to Propose Changes
editor.fork_create_description = You can not edit this repository directly. Instead you can create a fork, make edits and create a pull request.
editor.fork_edit_description = You can not edit this repository directly. The changes will be written to your fork <b>%s</b>, so you can create a pull request.
editor.fork_not_editable = You have forked this repository but your fork is not editable.
editor.fork_failed_to_push_branch = Failed to push branch %s to your repository.
editor.fork_branch_exists = Branch "%s" already exists in your fork, please choose a new branch name.
commits.desc = Browse source code change history. commits.desc = Browse source code change history.
commits.commits = Commits commits.commits = Commits
commits.no_commits = No commits in common. "%s" and "%s" have entirely different histories. commits.no_commits = No commits in common. "%s" and "%s" have entirely different histories.
@ -2946,6 +2958,7 @@ dashboard = Dashboard
self_check = Self Check self_check = Self Check
identity_access = Identity & Access identity_access = Identity & Access
users = User Accounts users = User Accounts
badges = Badges
organizations = Organizations organizations = Organizations
assets = Code Assets assets = Code Assets
repositories = Repositories repositories = Repositories
@ -3125,6 +3138,30 @@ emails.delete_desc = Are you sure you want to delete this email address?
emails.deletion_success = The email address has been deleted. emails.deletion_success = The email address has been deleted.
emails.delete_primary_email_error = You can not delete the primary email. emails.delete_primary_email_error = You can not delete the primary email.
badges.badges_manage_panel = Badge Management
badges.details = Badge Details
badges.new_badge = Create New Badge
badges.slug = Slug
badges.description = Description
badges.image_url = Image URL
badges.slug.must_fill = Slug must be filled.
badges.new_success = The badge "%s" has been created.
badges.update_success = The badge has been updated.
badges.deletion_success = The badge has been deleted.
badges.edit_badge = Edit Badge
badges.update_badge = Update Badge
badges.delete_badge = Delete Badge
badges.delete_badge_desc = Are you sure you want to permanently delete this badge?
badges.users_with_badge = Users with Badge (%s)
badges.add_user = Add User
badges.remove_user = Remove User
badges.delete_user_desc = Are you sure you want to remove this badge from the user?
badges.not_found = Badge not found!
badges.user_add_success = User has been added to the badge.
badges.user_remove_success = User has been removed from the badge.
badges.manage_users = Manage Users
orgs.org_manage_panel = Organization Management orgs.org_manage_panel = Organization Management
orgs.name = Name orgs.name = Name
orgs.teams = Teams orgs.teams = Teams
@ -3832,6 +3869,7 @@ runs.no_runs = The workflow has no runs yet.
runs.empty_commit_message = (empty commit message) runs.empty_commit_message = (empty commit message)
runs.expire_log_message = Logs have been purged because they were too old. runs.expire_log_message = Logs have been purged because they were too old.
runs.delete = Delete workflow run runs.delete = Delete workflow run
runs.cancel = Cancel workflow run
runs.delete.description = Are you sure you want to permanently delete this workflow run? This action cannot be undone. runs.delete.description = Are you sure you want to permanently delete this workflow run? This action cannot be undone.
runs.not_done = This workflow run is not done. runs.not_done = This workflow run is not done.
runs.view_workflow_file = View workflow file runs.view_workflow_file = View workflow file

View File

@ -1216,6 +1216,7 @@ editor.require_signed_commit=Esta rama requiere un commit firmado
editor.cherry_pick=Hacer Cherry-pick %s en: editor.cherry_pick=Hacer Cherry-pick %s en:
editor.revert=Revertir %s en: editor.revert=Revertir %s en:
commits.desc=Ver el historial de cambios de código fuente. commits.desc=Ver el historial de cambios de código fuente.
commits.commits=Commits commits.commits=Commits
commits.no_commits=No hay commits en común. "%s" y "%s" tienen historias totalmente diferentes. commits.no_commits=No hay commits en común. "%s" y "%s" tienen historias totalmente diferentes.

View File

@ -949,6 +949,7 @@ editor.no_commit_to_branch=نمی‌توان به طور مستقیم درمور
editor.user_no_push_to_branch=کاربر نمیتواند به شاخه ارسال کند editor.user_no_push_to_branch=کاربر نمیتواند به شاخه ارسال کند
editor.require_signed_commit=شاخه یک کامیت امضا شده لازم دارد editor.require_signed_commit=شاخه یک کامیت امضا شده لازم دارد
commits.desc=تاریخچه تغییرات کد منبع را مرور کنید. commits.desc=تاریخچه تغییرات کد منبع را مرور کنید.
commits.commits=کامیت‌ها commits.commits=کامیت‌ها
commits.nothing_to_compare=این شاخه ها برابرند. commits.nothing_to_compare=این شاخه ها برابرند.

View File

@ -764,6 +764,7 @@ editor.no_changes_to_show=Ei muutoksia näytettäväksi.
editor.add_subdir=Lisää hakemisto… editor.add_subdir=Lisää hakemisto…
editor.require_signed_commit=Haara vaatii vahvistetun commitin editor.require_signed_commit=Haara vaatii vahvistetun commitin
commits.commits=Commitit commits.commits=Commitit
commits.nothing_to_compare=Nämä haarat vastaavat toisiaan. commits.nothing_to_compare=Nämä haarat vastaavat toisiaan.
commits.search_all=Kaikki haarat commits.search_all=Kaikki haarat

View File

@ -1394,6 +1394,7 @@ editor.require_signed_commit=Cette branche nécessite une révision signée
editor.cherry_pick=Picorer %s vers: editor.cherry_pick=Picorer %s vers:
editor.revert=Rétablir %s sur: editor.revert=Rétablir %s sur:
commits.desc=Naviguer dans l'historique des modifications. commits.desc=Naviguer dans l'historique des modifications.
commits.commits=Révisions commits.commits=Révisions
commits.no_commits=Pas de révisions en commun. "%s" et "%s" ont des historiques entièrement différents. commits.no_commits=Pas de révisions en commun. "%s" et "%s" ont des historiques entièrement différents.

View File

@ -421,6 +421,7 @@ remember_me.compromised=Níl an comhartha logála isteach bailí níos mó a d'f
forgot_password_title=Dearmad ar an bPasfhocal forgot_password_title=Dearmad ar an bPasfhocal
forgot_password=Dearmad ar an bPasfhocal? forgot_password=Dearmad ar an bPasfhocal?
need_account=An bhfuil cuntas ag teastáil uait? need_account=An bhfuil cuntas ag teastáil uait?
sign_up_tip=Tá tú ag clárú an chéad chuntais sa chóras, a bhfuil pribhléidí riarthóra aige. Cuimhnigh go cúramach ar dainm úsáideora agus do phasfhocal. Má dhéanann tú dearmad ar an ainm úsáideora nó ar an pasfhocal, féach ar dhoiciméadacht Gitea le do thoil chun an cuntas a aisghabháil.
sign_up_now=Cláraigh anois. sign_up_now=Cláraigh anois.
sign_up_successful=Cruthaíodh cuntas go rathúil. Fáilte romhat! sign_up_successful=Cruthaíodh cuntas go rathúil. Fáilte romhat!
confirmation_mail_sent_prompt_ex=Tá ríomhphost dearbhaithe nua seolta chuig <b>%s</b>. Seiceáil do bhosca isteach laistigh den chéad %s eile chun an próiseas clárúcháin a chur i gcrích. Má tá do sheoladh ríomhphoist clárúcháin mícheart, is féidir leat síniú isteach arís agus é a athrú. confirmation_mail_sent_prompt_ex=Tá ríomhphost dearbhaithe nua seolta chuig <b>%s</b>. Seiceáil do bhosca isteach laistigh den chéad %s eile chun an próiseas clárúcháin a chur i gcrích. Má tá do sheoladh ríomhphoist clárúcháin mícheart, is féidir leat síniú isteach arís agus é a athrú.
@ -1373,6 +1374,7 @@ editor.branch_already_exists=Tá brainse "%s" ann cheana féin sa stóras seo.
editor.directory_is_a_file=Úsáidtear ainm eolaire "%s" cheana féin mar ainm comhaid sa stóras seo. editor.directory_is_a_file=Úsáidtear ainm eolaire "%s" cheana féin mar ainm comhaid sa stóras seo.
editor.file_is_a_symlink=Is nasc siombalach é "%s". Ní féidir naisc shiombalacha a chur in eagar san eagarthóir gréasáin editor.file_is_a_symlink=Is nasc siombalach é "%s". Ní féidir naisc shiombalacha a chur in eagar san eagarthóir gréasáin
editor.filename_is_a_directory=Úsáidtear ainm comhaid "%s" cheana féin mar ainm eolaire sa stóras seo. editor.filename_is_a_directory=Úsáidtear ainm comhaid "%s" cheana féin mar ainm eolaire sa stóras seo.
editor.file_modifying_no_longer_exists=Níl an comhad atá á mhodhnú, "%s", sa stóras seo a thuilleadh.
editor.file_changed_while_editing=Tá athrú tagtha ar ábhar an chomhad ó thosaigh tú ag eagarthóireacht <a target="_blank" rel="noopener noreferrer" href="%s">Cliceáil anseo</a> chun iad a fheiceáil nó Athru <strong>ithe a Tiomantas arís</strong> chun iad a fhorscríobh. editor.file_changed_while_editing=Tá athrú tagtha ar ábhar an chomhad ó thosaigh tú ag eagarthóireacht <a target="_blank" rel="noopener noreferrer" href="%s">Cliceáil anseo</a> chun iad a fheiceáil nó Athru <strong>ithe a Tiomantas arís</strong> chun iad a fhorscríobh.
editor.file_already_exists=Tá comhad darb ainm "%s" ann cheana féin sa stóras seo. editor.file_already_exists=Tá comhad darb ainm "%s" ann cheana féin sa stóras seo.
editor.commit_id_not_matching=Ní mheaitseálann an ID Tiomanta leis an ID nuair a thosaigh tú ag eagarthóireacht. Tiomanta isteach i mbrainse paiste agus ansin cumaisc. editor.commit_id_not_matching=Ní mheaitseálann an ID Tiomanta leis an ID nuair a thosaigh tú ag eagarthóireacht. Tiomanta isteach i mbrainse paiste agus ansin cumaisc.
@ -1393,6 +1395,9 @@ editor.user_no_push_to_branch=Ní féidir leis an úsáideoir brúigh go dtí an
editor.require_signed_commit=Éilíonn an Brainse tiomantas sínithe editor.require_signed_commit=Éilíonn an Brainse tiomantas sínithe
editor.cherry_pick=Roghnaigh silíní %s ar: editor.cherry_pick=Roghnaigh silíní %s ar:
editor.revert=Fill %s ar: editor.revert=Fill %s ar:
editor.failed_to_commit=Theip ar athruithe a chur i bhfeidhm.
editor.failed_to_commit_summary=Teachtaireacht Earráide:
commits.desc=Brabhsáil stair athraithe cód foinse. commits.desc=Brabhsáil stair athraithe cód foinse.
commits.commits=Tiomáintí commits.commits=Tiomáintí
@ -2807,6 +2812,7 @@ team_permission_desc=Cead
team_unit_desc=Ceadaigh Rochtain ar Rannóga Stóras team_unit_desc=Ceadaigh Rochtain ar Rannóga Stóras
team_unit_disabled=(Díchumasaithe) team_unit_disabled=(Díchumasaithe)
form.name_been_taken=Tá ainm na heagraíochta "%s" tógtha cheana féin.
form.name_reserved=Tá an t-ainm eagraíochta "%s" curtha in áirithe. form.name_reserved=Tá an t-ainm eagraíochta "%s" curtha in áirithe.
form.name_pattern_not_allowed=Ní cheadaítear an patrún "%s" in ainm eagraíochta. form.name_pattern_not_allowed=Ní cheadaítear an patrún "%s" in ainm eagraíochta.
form.create_org_not_allowed=Níl cead agat eagraíocht a chruthú. form.create_org_not_allowed=Níl cead agat eagraíocht a chruthú.
@ -2829,12 +2835,27 @@ settings.visibility.private_shortname=Príobháideach
settings.update_settings=Nuashonrú Socruithe settings.update_settings=Nuashonrú Socruithe
settings.update_setting_success=Nuashonraíodh socruithe eagraíochta. settings.update_setting_success=Nuashonraíodh socruithe eagraíochta.
settings.rename=Athainmnigh an Eagraíocht
settings.rename_desc=Má athraíonn tú ainm na heagraíochta, athrófar URL deagraíochta freisin agus saorfar an seanainm.
settings.rename_success=Athainmníodh an eagraíocht %[1]s go %[2]s go rathúil.
settings.rename_no_change=Níl aon athrú ar ainm na heagraíochta.
settings.rename_new_org_name=Ainm Nua na hEagraíochta
settings.rename_failed=Theip ar athainmniú na hEagraíochta mar gheall ar earráid inmheánach
settings.rename_notices_1=NÍ <strong>FÉIDIR</strong> an oibríocht seo a chealú.
settings.rename_notices_2=Déanfar an seanainm a atreorú go dtí go n-éileofar é.
settings.update_avatar_success=Nuashonraíodh avatar na heagraíochta. settings.update_avatar_success=Nuashonraíodh avatar na heagraíochta.
settings.delete=Scrios Eagraíocht settings.delete=Scrios Eagraíocht
settings.delete_account=Scrios an Eagraíocht seo settings.delete_account=Scrios an Eagraíocht seo
settings.delete_prompt=Bainfear an eagraíocht go buan. <strong>NÍ FÉIDIR</strong> é seo a chealú! settings.delete_prompt=Bainfear an eagraíocht go buan. <strong>NÍ FÉIDIR</strong> é seo a chealú!
settings.name_confirm=Cuir isteach ainm na heagraíochta mar dheimhniú:
settings.delete_notices_1=NÍ <strong>FÉIDIR</strong> an oibríocht seo a chealú.
settings.delete_notices_2=Scriosfaidh an oibríocht seo go buan gach <strong>stórais</strong> de chuid <strong>%s</strong>, lena n-áirítear cód, saincheisteanna, tuairimí, sonraí vicí agus socruithe comhoibritheora.
settings.delete_notices_3=Scriosfaidh an oibríocht seo gach <strong>pacáiste</strong> de chuid <strong>%s</strong> go buan.
settings.delete_notices_4=Scriosfaidh an oibríocht seo gach <strong>tionscadal</strong> de chuid <strong>%s</strong> go buan.
settings.confirm_delete_account=Deimhnigh scriosadh settings.confirm_delete_account=Deimhnigh scriosadh
settings.delete_failed=Theip ar Scriosadh na hEagraíochta mar gheall ar earráid inmheánach
settings.delete_successful=Scriosadh an eagraíocht <b>%s</b> go rathúil.
settings.hooks_desc=Cuir crúcaí gréasán in leis a spreagfar do <strong>gach stóras</strong> faoin eagraíocht seo. settings.hooks_desc=Cuir crúcaí gréasán in leis a spreagfar do <strong>gach stóras</strong> faoin eagraíocht seo.
settings.labels_desc=Cuir lipéid leis ar féidir iad a úsáid ar shaincheisteanna do <strong>gach stóras</strong> faoin eagraíocht seo. settings.labels_desc=Cuir lipéid leis ar féidir iad a úsáid ar shaincheisteanna do <strong>gach stóras</strong> faoin eagraíocht seo.

View File

@ -711,6 +711,7 @@ editor.commit_empty_file_header=Egy üres fájl commitolása
editor.no_changes_to_show=Nincsen megjeleníthető változás. editor.no_changes_to_show=Nincsen megjeleníthető változás.
editor.add_subdir=Mappa hozzáadása… editor.add_subdir=Mappa hozzáadása…
commits.commits=Commit-ok commits.commits=Commit-ok
commits.search_all=Minden ág commits.search_all=Minden ág
commits.author=Szerző commits.author=Szerző

View File

@ -717,6 +717,7 @@ editor.new_branch_name_desc=Nama branch baru…
editor.cancel=Membatalkan editor.cancel=Membatalkan
editor.no_changes_to_show=Tidak ada perubahan untuk ditampilkan. editor.no_changes_to_show=Tidak ada perubahan untuk ditampilkan.
commits.commits=Melakukan commits.commits=Melakukan
commits.author=Penulis commits.author=Penulis
commits.message=Pesan commits.message=Pesan

View File

@ -690,6 +690,7 @@ editor.create_new_branch_np=Búðu til <strong>nýja grein</strong> fyrir þetta
editor.new_branch_name_desc=Heiti nýjar greinar… editor.new_branch_name_desc=Heiti nýjar greinar…
editor.cancel=Hætta við editor.cancel=Hætta við
commits.commits=Framlög commits.commits=Framlög
commits.author=Höfundur commits.author=Höfundur
commits.message=Skilaboð commits.message=Skilaboð

View File

@ -1024,6 +1024,7 @@ editor.require_signed_commit=Il branch richiede un commit firmato
editor.cherry_pick=Cherry-pick %s suto: editor.cherry_pick=Cherry-pick %s suto:
editor.revert=Ripristina %s su: editor.revert=Ripristina %s su:
commits.desc=Sfoglia la cronologia di modifiche del codice rogente. commits.desc=Sfoglia la cronologia di modifiche del codice rogente.
commits.commits=Commit commits.commits=Commit
commits.nothing_to_compare=Questi rami sono uguali. commits.nothing_to_compare=Questi rami sono uguali.

View File

@ -421,6 +421,7 @@ remember_me.compromised=ログイントークンはもう有効ではなく、
forgot_password_title=パスワードを忘れた forgot_password_title=パスワードを忘れた
forgot_password=パスワードをお忘れですか? forgot_password=パスワードをお忘れですか?
need_account=アカウントが必要ですか? need_account=アカウントが必要ですか?
sign_up_tip=管理者権限を持つ、このシステムの最初のアカウントを登録しようとしています。 ユーザー名とパスワードをよく覚えておいてください。 ユーザー名またはパスワードを忘れた場合は、Giteaのドキュメントを参照してアカウントを復元してください。
sign_up_now=登録はこちら。 sign_up_now=登録はこちら。
sign_up_successful=アカウントは無事に作成されました。ようこそ! sign_up_successful=アカウントは無事に作成されました。ようこそ!
confirmation_mail_sent_prompt_ex=新しい確認メールを <b>%s</b> に送信しました。 %s以内にメールボックスを確認し、登録手続きを完了してください。 登録メールアドレスが間違っている場合は、もういちどサインインすると変更することができます。 confirmation_mail_sent_prompt_ex=新しい確認メールを <b>%s</b> に送信しました。 %s以内にメールボックスを確認し、登録手続きを完了してください。 登録メールアドレスが間違っている場合は、もういちどサインインすると変更することができます。
@ -1332,7 +1333,9 @@ editor.upload_file=ファイルをアップロード
editor.edit_file=ファイルを編集 editor.edit_file=ファイルを編集
editor.preview_changes=変更をプレビュー editor.preview_changes=変更をプレビュー
editor.cannot_edit_lfs_files=LFSのファイルはWebインターフェースで編集できません。 editor.cannot_edit_lfs_files=LFSのファイルはWebインターフェースで編集できません。
editor.cannot_edit_too_large_file=このファイルは大きすぎるため、編集できません。
editor.cannot_edit_non_text_files=バイナリファイルはWebインターフェースで編集できません。 editor.cannot_edit_non_text_files=バイナリファイルはWebインターフェースで編集できません。
editor.file_not_editable_hint=名前の変更や移動は可能です。
editor.edit_this_file=ファイルを編集 editor.edit_this_file=ファイルを編集
editor.this_file_locked=ファイルはロックされています editor.this_file_locked=ファイルはロックされています
editor.must_be_on_a_branch=このファイルを変更したり変更の提案をするには、ブランチ上にいる必要があります。 editor.must_be_on_a_branch=このファイルを変更したり変更の提案をするには、ブランチ上にいる必要があります。
@ -1352,6 +1355,7 @@ editor.update=%s を更新
editor.delete=%s を削除 editor.delete=%s を削除
editor.patch=パッチの適用 editor.patch=パッチの適用
editor.patching=パッチ: editor.patching=パッチ:
editor.fail_to_apply_patch=パッチを適用できません
editor.new_patch=新しいパッチ editor.new_patch=新しいパッチ
editor.commit_message_desc=詳細な説明を追加… editor.commit_message_desc=詳細な説明を追加…
editor.signoff_desc=コミットログメッセージの最後にコミッターの Signed-off-by 行を追加 editor.signoff_desc=コミットログメッセージの最後にコミッターの Signed-off-by 行を追加
@ -1371,6 +1375,7 @@ editor.branch_already_exists=ブランチ "%s" は、このリポジトリに既
editor.directory_is_a_file=ディレクトリ名 "%s" はすでにリポジトリ内のファイルで使用されています。 editor.directory_is_a_file=ディレクトリ名 "%s" はすでにリポジトリ内のファイルで使用されています。
editor.file_is_a_symlink=`"%s" はシンボリックリンクです。 シンボリックリンクはWebエディターで編集できません。` editor.file_is_a_symlink=`"%s" はシンボリックリンクです。 シンボリックリンクはWebエディターで編集できません。`
editor.filename_is_a_directory=ファイル名 "%s" は、このリポジトリ上でディレクトリ名としてすでに使用されています。 editor.filename_is_a_directory=ファイル名 "%s" は、このリポジトリ上でディレクトリ名としてすでに使用されています。
editor.file_modifying_no_longer_exists=修正中のファイル "%s" が、すでにリポジトリ内にありません。
editor.file_changed_while_editing=あなたが編集を開始したあと、ファイルの内容が変更されました。 <a target="_blank" rel="noopener noreferrer" href="%s">ここをクリック</a>して何が変更されたか確認するか、<strong>もう一度"変更をコミット"をクリック</strong>して上書きします。 editor.file_changed_while_editing=あなたが編集を開始したあと、ファイルの内容が変更されました。 <a target="_blank" rel="noopener noreferrer" href="%s">ここをクリック</a>して何が変更されたか確認するか、<strong>もう一度"変更をコミット"をクリック</strong>して上書きします。
editor.file_already_exists=ファイル "%s" は、このリポジトリに既に存在します。 editor.file_already_exists=ファイル "%s" は、このリポジトリに既に存在します。
editor.commit_id_not_matching=コミットIDが編集を開始したときのIDと一致しません。 パッチ用のブランチにコミットしたあとマージしてください。 editor.commit_id_not_matching=コミットIDが編集を開始したときのIDと一致しません。 パッチ用のブランチにコミットしたあとマージしてください。
@ -1391,6 +1396,9 @@ editor.user_no_push_to_branch=ユーザーはブランチにプッシュでき
editor.require_signed_commit=ブランチでは署名されたコミットが必須です editor.require_signed_commit=ブランチでは署名されたコミットが必須です
editor.cherry_pick=チェリーピック %s: editor.cherry_pick=チェリーピック %s:
editor.revert=リバート %s: editor.revert=リバート %s:
editor.failed_to_commit=変更のコミットに失敗しました。
editor.failed_to_commit_summary=エラーメッセージ:
commits.desc=ソースコードの変更履歴を参照します。 commits.desc=ソースコードの変更履歴を参照します。
commits.commits=コミット commits.commits=コミット
@ -1555,6 +1563,7 @@ issues.filter_user_placeholder=ユーザーを検索
issues.filter_user_no_select=すべてのユーザー issues.filter_user_no_select=すべてのユーザー
issues.filter_type=タイプ issues.filter_type=タイプ
issues.filter_type.all_issues=すべてのイシュー issues.filter_type.all_issues=すべてのイシュー
issues.filter_type.all_pull_requests=すべてのプルリクエスト
issues.filter_type.assigned_to_you=自分が担当 issues.filter_type.assigned_to_you=自分が担当
issues.filter_type.created_by_you=自分が作成 issues.filter_type.created_by_you=自分が作成
issues.filter_type.mentioning_you=自分が関係 issues.filter_type.mentioning_you=自分が関係
@ -1646,6 +1655,7 @@ issues.save=保存
issues.label_title=名前 issues.label_title=名前
issues.label_description=説明 issues.label_description=説明
issues.label_color=カラー issues.label_color=カラー
issues.label_color_invalid=無効な色です
issues.label_exclusive=排他 issues.label_exclusive=排他
issues.label_archive=アーカイブ ラベル issues.label_archive=アーカイブ ラベル
issues.label_archived_filter=アーカイブされたラベルを表示 issues.label_archived_filter=アーカイブされたラベルを表示
@ -2394,6 +2404,8 @@ settings.event_pull_request_review_request_desc=プルリクエストのレビ
settings.event_pull_request_approvals=プルリクエストの承認 settings.event_pull_request_approvals=プルリクエストの承認
settings.event_pull_request_merge=プルリクエストのマージ settings.event_pull_request_merge=プルリクエストのマージ
settings.event_header_workflow=ワークフローイベント settings.event_header_workflow=ワークフローイベント
settings.event_workflow_run=ワークフロー実行
settings.event_workflow_run_desc=Gitea Actions のワークフロー実行が、キューに追加、待機中、実行中、完了になったとき。
settings.event_workflow_job=ワークフロージョブ settings.event_workflow_job=ワークフロージョブ
settings.event_workflow_job_desc=Gitea Actions のワークフロージョブが、キューに追加、待機中、実行中、完了になったとき。 settings.event_workflow_job_desc=Gitea Actions のワークフロージョブが、キューに追加、待機中、実行中、完了になったとき。
settings.event_package=パッケージ settings.event_package=パッケージ
@ -2801,6 +2813,7 @@ team_permission_desc=権限
team_unit_desc=リポジトリのセクションへのアクセスを許可 team_unit_desc=リポジトリのセクションへのアクセスを許可
team_unit_disabled=(無効) team_unit_disabled=(無効)
form.name_been_taken=組織名 "%s" は既に使用されています。
form.name_reserved=組織名 "%s" は予約されています。 form.name_reserved=組織名 "%s" は予約されています。
form.name_pattern_not_allowed=`"%s" の形式は組織名に使用できません。` form.name_pattern_not_allowed=`"%s" の形式は組織名に使用できません。`
form.create_org_not_allowed=組織を作成する権限がありません。 form.create_org_not_allowed=組織を作成する権限がありません。
@ -2823,12 +2836,27 @@ settings.visibility.private_shortname=プライベート
settings.update_settings=設定の更新 settings.update_settings=設定の更新
settings.update_setting_success=組織の設定を更新しました。 settings.update_setting_success=組織の設定を更新しました。
settings.rename=組織名の変更
settings.rename_desc=組織名を変更すると組織のURLも変更され、古い名前は解放されます。
settings.rename_success=組織 %[1]s の %[2]s への改名に成功しました。
settings.rename_no_change=組織名の変更はありません。
settings.rename_new_org_name=新しい組織名
settings.rename_failed=内部エラーのため組織名を変更できませんでした
settings.rename_notices_1=この操作は<strong>元に戻せません</strong> 。
settings.rename_notices_2=古い名前は、再使用されるまではリダイレクトします。
settings.update_avatar_success=組織のアバターを更新しました。 settings.update_avatar_success=組織のアバターを更新しました。
settings.delete=組織を削除 settings.delete=組織を削除
settings.delete_account=この組織を削除 settings.delete_account=この組織を削除
settings.delete_prompt=組織は恒久的に削除されます。 元に戻すことは<strong>できません</strong> settings.delete_prompt=組織は恒久的に削除されます。 元に戻すことは<strong>できません</strong>
settings.name_confirm=確認のため組織名を入力:
settings.delete_notices_1=この操作は<strong>元に戻せません</strong> 。
settings.delete_notices_2=この操作により、<strong>%s</strong>のすべての<strong>リポジトリ</strong>が恒久的に削除されます。 コード、イシュー、コメント、Wikiデータ、共同作業者の設定も含まれます。
settings.delete_notices_3=この操作により、<strong>%s</strong>のすべての<strong>パッケージ</strong>が恒久的に削除されます。
settings.delete_notices_4=この操作により、<strong>%s</strong>のすべての<strong>プロジェクト</strong>が恒久的に削除されます。
settings.confirm_delete_account=削除を確認 settings.confirm_delete_account=削除を確認
settings.delete_failed=内部エラーのため組織を削除できませんでした
settings.delete_successful=組織の<b>%s</b>の削除に成功しました。
settings.hooks_desc=この組織の<strong>すべてのリポジトリ</strong>でトリガーされるWebhookを追加します。 settings.hooks_desc=この組織の<strong>すべてのリポジトリ</strong>でトリガーされるWebhookを追加します。
settings.labels_desc=この組織の<strong>すべてのリポジトリ</strong>で使用可能なイシューラベルを追加します。 settings.labels_desc=この組織の<strong>すべてのリポジトリ</strong>で使用可能なイシューラベルを追加します。

View File

@ -648,6 +648,7 @@ editor.filename_cannot_be_empty=파일명이 빈칸입니다.
editor.no_changes_to_show=표시할 변경사항이 없습니다. editor.no_changes_to_show=표시할 변경사항이 없습니다.
editor.add_subdir=경로 추가... editor.add_subdir=경로 추가...
commits.desc=소스 코드 변경 내역 탐색 commits.desc=소스 코드 변경 내역 탐색
commits.commits=커밋 commits.commits=커밋
commits.search_all=모든 브랜치 commits.search_all=모든 브랜치

View File

@ -1232,6 +1232,7 @@ editor.require_signed_commit=Atzarā var iesūtīt tikai parakstītas revīzijas
editor.cherry_pick=Izlasīt %s uz: editor.cherry_pick=Izlasīt %s uz:
editor.revert=Atgriezt %s uz: editor.revert=Atgriezt %s uz:
commits.desc=Pārlūkot pirmkoda izmaiņu vēsturi. commits.desc=Pārlūkot pirmkoda izmaiņu vēsturi.
commits.commits=Revīzijas commits.commits=Revīzijas
commits.no_commits=Nav kopīgu revīziju. Atzariem "%s" un "%s" ir pilnībā atšķirīga izmaiņu vēsture. commits.no_commits=Nav kopīgu revīziju. Atzariem "%s" un "%s" ir pilnībā atšķirīga izmaiņu vēsture.

View File

@ -1022,6 +1022,7 @@ editor.require_signed_commit=Branch vereist een ondertekende commit
editor.cherry_pick=Cherry-pick %s op: editor.cherry_pick=Cherry-pick %s op:
editor.revert=%s ongedaan maken op: editor.revert=%s ongedaan maken op:
commits.desc=Bekijk de broncode-wijzigingsgeschiedenis. commits.desc=Bekijk de broncode-wijzigingsgeschiedenis.
commits.commits=Commits commits.commits=Commits
commits.nothing_to_compare=Deze branches zijn gelijk. commits.nothing_to_compare=Deze branches zijn gelijk.

View File

@ -948,6 +948,7 @@ editor.no_commit_to_branch=Zatwierdzanie bezpośrednio do tej gałęzi nie jest
editor.user_no_push_to_branch=Użytkownik nie może wypychać do gałęzi editor.user_no_push_to_branch=Użytkownik nie może wypychać do gałęzi
editor.require_signed_commit=Gałąź wymaga podpisanych commitów editor.require_signed_commit=Gałąź wymaga podpisanych commitów
commits.desc=Przeglądaj historię zmian kodu źródłowego. commits.desc=Przeglądaj historię zmian kodu źródłowego.
commits.commits=Commity commits.commits=Commity
commits.search_all=Wszystkie gałęzie commits.search_all=Wszystkie gałęzie

View File

@ -1227,6 +1227,7 @@ editor.require_signed_commit=Branch requer um commit assinado
editor.cherry_pick=Cherry-pick %s para: editor.cherry_pick=Cherry-pick %s para:
editor.revert=Reverter %s para: editor.revert=Reverter %s para:
commits.desc=Veja o histórico de alterações do código de fonte. commits.desc=Veja o histórico de alterações do código de fonte.
commits.commits=Commits commits.commits=Commits
commits.no_commits=Nenhum commit em comum. "%s" e "%s" tem históricos completamente diferentes. commits.no_commits=Nenhum commit em comum. "%s" e "%s" tem históricos completamente diferentes.

View File

@ -1393,6 +1393,7 @@ editor.require_signed_commit=O ramo requer um cometimento assinado
editor.cherry_pick=Escolher a dedo %s para: editor.cherry_pick=Escolher a dedo %s para:
editor.revert=Reverter %s para: editor.revert=Reverter %s para:
commits.desc=Navegar pelo histórico de modificações no código fonte. commits.desc=Navegar pelo histórico de modificações no código fonte.
commits.commits=Cometimentos commits.commits=Cometimentos
commits.no_commits=Não há cometimentos em comum. "%s" e "%s" têm históricos completamente diferentes. commits.no_commits=Não há cometimentos em comum. "%s" e "%s" têm históricos completamente diferentes.

View File

@ -1205,6 +1205,7 @@ editor.require_signed_commit=Ветка ожидает подписанный к
editor.cherry_pick=Перенести изменения %s в: editor.cherry_pick=Перенести изменения %s в:
editor.revert=Откатить %s к: editor.revert=Откатить %s к:
commits.desc=Просмотр истории изменений исходного кода. commits.desc=Просмотр истории изменений исходного кода.
commits.commits=Коммитов commits.commits=Коммитов
commits.no_commits=Нет общих коммитов. «%s» и «%s» имеют совершенно разные истории. commits.no_commits=Нет общих коммитов. «%s» и «%s» имеют совершенно разные истории.

View File

@ -923,6 +923,7 @@ editor.no_commit_to_branch=ශාඛාවට කෙලින්ම කැපව
editor.user_no_push_to_branch=පරිශීලකයාට ශාඛාවට තල්ලු කළ නොහැක editor.user_no_push_to_branch=පරිශීලකයාට ශාඛාවට තල්ලු කළ නොහැක
editor.require_signed_commit=ශාඛාවට අත්සන් කළ කැපවීමක් අවශ්ය වේ editor.require_signed_commit=ශාඛාවට අත්සන් කළ කැපවීමක් අවශ්ය වේ
commits.desc=මූලාශ්ර කේත වෙනස් කිරීමේ ඉතිහාසය පිරික්සන්න. commits.desc=මූලාශ්ර කේත වෙනස් කිරීමේ ඉතිහාසය පිරික්සන්න.
commits.commits=විවරයන් commits.commits=විවරයන්
commits.nothing_to_compare=මෙම ශාඛා සමාන වේ. commits.nothing_to_compare=මෙම ශාඛා සමාන වේ.

View File

@ -1007,6 +1007,7 @@ editor.commit_empty_file_text=Súbor, ktorý sa chystáte odoslať, je prázdny.
editor.no_commit_to_branch=Nedá sa odoslať priamo do vetvy, pretože: editor.no_commit_to_branch=Nedá sa odoslať priamo do vetvy, pretože:
editor.require_signed_commit=Vetva vyžaduje podpísaný commit editor.require_signed_commit=Vetva vyžaduje podpísaný commit
commits.commits=Commity commits.commits=Commity
commits.search_all=Všetky vetvy commits.search_all=Všetky vetvy
commits.author=Autor commits.author=Autor

View File

@ -782,6 +782,7 @@ editor.no_commit_to_branch=Det gick inte att committa direkt till branchen för:
editor.user_no_push_to_branch=Användaren kan inte pusha till branchen editor.user_no_push_to_branch=Användaren kan inte pusha till branchen
editor.require_signed_commit=Branchen kräver en signerad commit editor.require_signed_commit=Branchen kräver en signerad commit
commits.desc=Bläddra i källkodens förändringshistorik. commits.desc=Bläddra i källkodens förändringshistorik.
commits.commits=Incheckningar commits.commits=Incheckningar
commits.search_all=Alla brancher commits.search_all=Alla brancher

View File

@ -1385,6 +1385,7 @@ editor.require_signed_commit=Dal imzalı bir işleme gerektirir
editor.cherry_pick=%s şunun üzerine cımbızla: editor.cherry_pick=%s şunun üzerine cımbızla:
editor.revert=%s şuna geri döndür: editor.revert=%s şuna geri döndür:
commits.desc=Kaynak kodu değişiklik geçmişine göz atın. commits.desc=Kaynak kodu değişiklik geçmişine göz atın.
commits.commits=İşleme commits.commits=İşleme
commits.no_commits=Ortak bir işleme yok. "%s" ve "%s" tamamen farklı geçmişlere sahip. commits.no_commits=Ortak bir işleme yok. "%s" ve "%s" tamamen farklı geçmişlere sahip.

View File

@ -92,6 +92,7 @@ remove=Видалити
remove_all=Видалити все remove_all=Видалити все
remove_label_str=`Видалити елемент "%s"` remove_label_str=`Видалити елемент "%s"`
edit=Редагувати edit=Редагувати
view=Переглянути
test=Тест test=Тест
enabled=Увімкнено enabled=Увімкнено
@ -414,6 +415,7 @@ remember_me.compromised=Токен для входу більше не дійс
forgot_password_title=Забув пароль forgot_password_title=Забув пароль
forgot_password=Забули пароль? forgot_password=Забули пароль?
need_account=Потрібен обліковий запис? need_account=Потрібен обліковий запис?
sign_up_tip=Ви реєструєте перший обліковий запис у системі, з правами адміністратора. Будь ласка, уважно запам'ятайте своє ім'я користувача та пароль. Якщо ви їх забудете, зверніться до документації Gitea, щоб відновити обліковий запис.
sign_up_now=Зареєструватися. sign_up_now=Зареєструватися.
sign_up_successful=Обліковий запис створено успішно. Вітаю! sign_up_successful=Обліковий запис створено успішно. Вітаю!
confirmation_mail_sent_prompt_ex=Новий лист з підтвердженням було надіслано на <b>%s</b>. Будь ласка, перевірте свою поштову скриньку протягом наступних %s, щоб завершити процес реєстрації. Якщо ви вказали невірну адресу електронної пошти, ви можете увійти ще раз і змінити її. confirmation_mail_sent_prompt_ex=Новий лист з підтвердженням було надіслано на <b>%s</b>. Будь ласка, перевірте свою поштову скриньку протягом наступних %s, щоб завершити процес реєстрації. Якщо ви вказали невірну адресу електронної пошти, ви можете увійти ще раз і змінити її.
@ -970,6 +972,7 @@ passcode_invalid=Некоректний пароль. Спробуй ще раз
twofa_failed_get_secret=Не вдалося отримати код. twofa_failed_get_secret=Не вдалося отримати код.
webauthn_register_key=Додати ключ безпеки webauthn_register_key=Додати ключ безпеки
webauthn_nickname=Псевдонім
webauthn_delete_key=Видалити ключ безпеки webauthn_delete_key=Видалити ключ безпеки
webauthn_delete_key_desc=Якщо ви видалите ключ безпеки, ви більше не зможете ввійти за його допомогою. Продовжити? webauthn_delete_key_desc=Якщо ви видалите ключ безпеки, ви більше не зможете ввійти за його допомогою. Продовжити?
webauthn_key_loss_warning=Якщо ви втратите ключі безпеки, ви втратите доступ до свого облікового запису. webauthn_key_loss_warning=Якщо ви втратите ключі безпеки, ви втратите доступ до свого облікового запису.
@ -1336,6 +1339,7 @@ editor.commit_email=Електронна пошта коміту
editor.invalid_commit_email=Адреса електронної пошти для коміту недійсна. editor.invalid_commit_email=Адреса електронної пошти для коміту недійсна.
editor.file_is_a_symlink=`"%s" - це символічне посилання. Символічні посилання не можна редагувати у веб-редакторі` editor.file_is_a_symlink=`"%s" - це символічне посилання. Символічні посилання не можна редагувати у веб-редакторі`
editor.filename_is_a_directory=Назва файлу '%s' вже використовується як назва каталогу у цьому сховищі. editor.filename_is_a_directory=Назва файлу '%s' вже використовується як назва каталогу у цьому сховищі.
editor.file_modifying_no_longer_exists=Редагований файл '%s' більше не існує в цьому сховищі.
editor.file_changed_while_editing=Зміст файлу змінився з моменту початку редагування. <a target="_blank" rel="noopener" href="%s"> Натисніть тут </a>, щоб переглянути що було змінено, або <strong>закомітьте зміни ще раз</strong>, щоб переписати їх. editor.file_changed_while_editing=Зміст файлу змінився з моменту початку редагування. <a target="_blank" rel="noopener" href="%s"> Натисніть тут </a>, щоб переглянути що було змінено, або <strong>закомітьте зміни ще раз</strong>, щоб переписати їх.
editor.commit_empty_file_header=Закомітити порожній файл editor.commit_empty_file_header=Закомітити порожній файл
editor.commit_empty_file_text=Файл, який ви збираєтеся закомітити, порожній. Продовжувати? editor.commit_empty_file_text=Файл, який ви збираєтеся закомітити, порожній. Продовжувати?
@ -1349,9 +1353,14 @@ editor.upload_file_is_locked=Файл "%s" заблоковано %s.
editor.upload_files_to_dir=`Завантажити файли до "%s"` editor.upload_files_to_dir=`Завантажити файли до "%s"`
editor.no_commit_to_branch=Не вдалося внести коміт безпосередньо до гілки, тому що: editor.no_commit_to_branch=Не вдалося внести коміт безпосередньо до гілки, тому що:
editor.require_signed_commit=Гілка вимагає підписаного коміту editor.require_signed_commit=Гілка вимагає підписаного коміту
editor.revert=Повернути %s до:
editor.failed_to_commit=Не вдалося зафіксувати зміни.
editor.failed_to_commit_summary=Помилка:
commits.desc=Переглянути історію зміни коду. commits.desc=Переглянути історію зміни коду.
commits.commits=Коміти commits.commits=Коміти
commits.no_commits=Немає спільних комітів. '%s' та '%s' мають різну історію.
commits.nothing_to_compare=Ці гілки однакові. commits.nothing_to_compare=Ці гілки однакові.
commits.search_branch=Ця гілка commits.search_branch=Ця гілка
commits.search_all=Усі гілки commits.search_all=Усі гілки
@ -1368,6 +1377,7 @@ commits.ssh_key_fingerprint=Відбиток ключа SSH
commits.view_path=Переглянути в історії commits.view_path=Переглянути в історії
commits.view_file_diff=Переглянути зміни до цього файлу в цьому коміті commits.view_file_diff=Переглянути зміни до цього файлу в цьому коміті
commit.operations=Дії
commit.revert=Повернути до попереднього стану commit.revert=Повернути до попереднього стану
commit.revert-header=Повернути: %s commit.revert-header=Повернути: %s
commit.revert-content=Виберіть гілку, до якої хочете повернутися: commit.revert-content=Виберіть гілку, до якої хочете повернутися:
@ -1442,6 +1452,7 @@ issues.new.clear_assignees=Прибрати виконавців
issues.new.no_assignees=Немає виконавців issues.new.no_assignees=Немає виконавців
issues.new.no_reviewers=Немає рецензентів issues.new.no_reviewers=Немає рецензентів
issues.new.blocked_user=Не вдалося створити задачу, тому що ви заблоковані власником сховища. issues.new.blocked_user=Не вдалося створити задачу, тому що ви заблоковані власником сховища.
issues.edit.blocked_user=Неможливо редагувати вміст, оскільки вас заблоковано автором або власником сховища.
issues.choose.get_started=Розпочати issues.choose.get_started=Розпочати
issues.choose.open_external_link=Відкрити issues.choose.open_external_link=Відкрити
issues.choose.blank=Типово issues.choose.blank=Типово
@ -1459,6 +1470,7 @@ issues.label_templates.title=Завантажити визначений наб
issues.label_templates.info=Ще немає міток. Натисніть 'Нова мітка' або використовуйте попередньо визначений набір міток: issues.label_templates.info=Ще немає міток. Натисніть 'Нова мітка' або використовуйте попередньо визначений набір міток:
issues.label_templates.helper=Оберіть набір міток issues.label_templates.helper=Оберіть набір міток
issues.label_templates.use=Використати набір міток issues.label_templates.use=Використати набір міток
issues.label_templates.fail_to_load_file=Не вдалося завантажити файл шаблона мітки '%s': %v
issues.add_label=додано %s з міткою %s issues.add_label=додано %s з міткою %s
issues.add_labels=додано %s з мітками %s issues.add_labels=додано %s з мітками %s
issues.remove_label=видалено %s з міткою %s issues.remove_label=видалено %s з міткою %s
@ -1494,6 +1506,7 @@ issues.filter_project=Проєкт
issues.filter_project_all=Всі проєкти issues.filter_project_all=Всі проєкти
issues.filter_project_none=Проєкт відсутній issues.filter_project_none=Проєкт відсутній
issues.filter_assignee=Виконавець issues.filter_assignee=Виконавець
issues.filter_assignee_no_assignee=Нікому не присвоєно
issues.filter_assignee_any_assignee=Призначено будь-кому issues.filter_assignee_any_assignee=Призначено будь-кому
issues.filter_poster=Автор issues.filter_poster=Автор
issues.filter_user_placeholder=Пошук користувачів issues.filter_user_placeholder=Пошук користувачів
@ -1504,6 +1517,7 @@ issues.filter_type.all_pull_requests=Усі запити на злиття
issues.filter_type.assigned_to_you=Призначене вам issues.filter_type.assigned_to_you=Призначене вам
issues.filter_type.created_by_you=Створено вами issues.filter_type.created_by_you=Створено вами
issues.filter_type.mentioning_you=Вас згадано issues.filter_type.mentioning_you=Вас згадано
issues.filter_type.review_requested=Запит на рецензію
issues.filter_type.reviewed_by_you=Перевірено вами issues.filter_type.reviewed_by_you=Перевірено вами
issues.filter_sort=Сортувати issues.filter_sort=Сортувати
issues.filter_sort.latest=Найновіші issues.filter_sort.latest=Найновіші
@ -1543,7 +1557,9 @@ issues.context.quote_reply=Цитувати відповідь
issues.context.reference_issue=Посилання в новій задачі issues.context.reference_issue=Посилання в новій задачі
issues.context.edit=Редагувати issues.context.edit=Редагувати
issues.context.delete=Видалити issues.context.delete=Видалити
issues.no_content=Немає опису.
issues.close=Закрити задачу issues.close=Закрити задачу
issues.comment_pull_merged_at=об'єднав(-ла) коміти %[1]s в %[2]s %[3]s
issues.comment_manually_pull_merged_at=вручну об'єднав(-ла) коміти %[1]s в %[2]s %[3]s issues.comment_manually_pull_merged_at=вручну об'єднав(-ла) коміти %[1]s в %[2]s %[3]s
issues.close_comment_issue=Закрити з коментарем issues.close_comment_issue=Закрити з коментарем
issues.reopen_issue=Відкрити знову issues.reopen_issue=Відкрити знову
@ -1586,6 +1602,7 @@ issues.label_title=Назва мітки
issues.label_description=Опис мітки issues.label_description=Опис мітки
issues.label_color=Колір issues.label_color=Колір
issues.label_color_invalid=Недійсний колір issues.label_color_invalid=Недійсний колір
issues.label_exclusive=Ексклюзивно
issues.label_archive=Мітка архіву issues.label_archive=Мітка архіву
issues.label_archived_filter=Показати архівовані мітки issues.label_archived_filter=Показати архівовані мітки
issues.label_archive_tooltip=Архівовані мітки типово виключаються з пропозицій під час пошуку за мітками. issues.label_archive_tooltip=Архівовані мітки типово виключаються з пропозицій під час пошуку за мітками.
@ -1777,7 +1794,13 @@ pulls.switch_comparison_type=Перемкнути тип порівняння
pulls.switch_head_and_base=Поміняти місцями основну та базову гілку pulls.switch_head_and_base=Поміняти місцями основну та базову гілку
pulls.filter_branch=Фільтр по гілці pulls.filter_branch=Фільтр по гілці
pulls.show_all_commits=Показати всі коміти pulls.show_all_commits=Показати всі коміти
pulls.show_changes_since_your_last_review=Показати зміни після вашого останнього відгуку
pulls.showing_only_single_commit=Відображаються лише зміни коміту %[1]s
pulls.showing_specified_commit_range=Відображаються лише зміни між %[1]s..%[2]s
pulls.select_commit_hold_shift_for_range=Виберіть коміт. Натисніть клавішу Shift + клацніть, щоб виділити діапазон
pulls.filter_changes_by_commit=Фільтр за комітом
pulls.nothing_to_compare=Ці гілки однакові. Немає необхідності створювати запитів на злиття. pulls.nothing_to_compare=Ці гілки однакові. Немає необхідності створювати запитів на злиття.
pulls.nothing_to_compare_have_tag=Виділена гілка або мітка ідентичні.
pulls.nothing_to_compare_and_allow_empty_pr=Одинакові гілки. Цей PR буде порожнім. pulls.nothing_to_compare_and_allow_empty_pr=Одинакові гілки. Цей PR буде порожнім.
pulls.has_pull_request=`Запит злиття для цих гілок вже існує: <a href="%[1]s">%[2]s#%[3]d</a>` pulls.has_pull_request=`Запит злиття для цих гілок вже існує: <a href="%[1]s">%[2]s#%[3]d</a>`
pulls.create=Створити запит на злиття pulls.create=Створити запит на злиття
@ -1790,6 +1813,7 @@ pulls.tab_files=Змінені файли
pulls.reopen_to_merge=Будь ласка, заново відкрийте цей запит щоб виконати злиття. pulls.reopen_to_merge=Будь ласка, заново відкрийте цей запит щоб виконати злиття.
pulls.cant_reopen_deleted_branch=Цей запит не можна повторно відкрити, оскільки гілку видалено. pulls.cant_reopen_deleted_branch=Цей запит не можна повторно відкрити, оскільки гілку видалено.
pulls.merged=Злито pulls.merged=Злито
pulls.merged_success=Запит на злиття успішно об'єднано і закрито
pulls.closed=Запит на злиття закрито pulls.closed=Запит на злиття закрито
pulls.manually_merged=Ручне злиття pulls.manually_merged=Ручне злиття
pulls.merged_info_text=Гілку %s тепер можна видалити. pulls.merged_info_text=Гілку %s тепер можна видалити.
@ -1802,6 +1826,7 @@ pulls.remove_prefix=Видалити префікс <strong>%s</strong>
pulls.data_broken=Збій цього запиту на злиття через відсутність інформації про форк. pulls.data_broken=Збій цього запиту на злиття через відсутність інформації про форк.
pulls.files_conflicted=Цей запит на злиття має зміни, що конфліктують з цільовою гілкою. pulls.files_conflicted=Цей запит на злиття має зміни, що конфліктують з цільовою гілкою.
pulls.is_checking=Перевірка конфліктів об'єднання (merge) ... pulls.is_checking=Перевірка конфліктів об'єднання (merge) ...
pulls.is_ancestor=Цю гілку вже включено до цільової гілки. Нема чого об'єднувати.
pulls.required_status_check_failed=Деякі необхідні перевірки виконані з помилками. pulls.required_status_check_failed=Деякі необхідні перевірки виконані з помилками.
pulls.required_status_check_missing=Декілька з необхідних перевірок відсутні. pulls.required_status_check_missing=Декілька з необхідних перевірок відсутні.
pulls.required_status_check_administrator=Як адміністратор ви все одно можете об'єднати цей запит на злиття. pulls.required_status_check_administrator=Як адміністратор ви все одно можете об'єднати цей запит на злиття.
@ -1869,6 +1894,9 @@ pulls.auto_merge_not_scheduled=Цей запит на злиття не план
pulls.delete.title=Видалити цей запит на злиття? pulls.delete.title=Видалити цей запит на злиття?
pulls.upstream_diverging_prompt_behind_1=Ця гілка на %[1]d коміт позаду %[2]s
pulls.upstream_diverging_prompt_behind_n=Ця гілка на %[1]d комітів позаду %[2]s
pulls.upstream_diverging_prompt_base_newer=Базова гілка %s має нові зміни
pulls.upstream_diverging_merge_confirm=Хочете об’єднати "%[1]s" з "%[2]s"? pulls.upstream_diverging_merge_confirm=Хочете об’єднати "%[1]s" з "%[2]s"?
pull.deleted_branch=(видалена):%s pull.deleted_branch=(видалена):%s
@ -2248,6 +2276,10 @@ settings.event_pull_request_review=Запит на злиття рецензов
settings.event_pull_request_review_desc=Запит на злиття підтверджено, відхилено або прокоментовано. settings.event_pull_request_review_desc=Запит на злиття підтверджено, відхилено або прокоментовано.
settings.event_pull_request_sync=Запит на злиття синхронізується settings.event_pull_request_sync=Запит на злиття синхронізується
settings.event_pull_request_sync_desc=Запит до злиття синхронізовано. settings.event_pull_request_sync_desc=Запит до злиття синхронізовано.
settings.event_header_workflow=Події робочого процесу
settings.event_workflow_run=Запущений робочий процес
settings.event_workflow_run_desc=Запущений робочий процес Gitea в черзі, в очікуванні, в процесі виконання або завершений.
settings.event_workflow_job=Завдання робочого процесу
settings.event_package=Пакет settings.event_package=Пакет
settings.branch_filter=Фільтр гілок settings.branch_filter=Фільтр гілок
settings.authorization_header=Заголовок авторизації settings.authorization_header=Заголовок авторизації
@ -2586,6 +2618,7 @@ team_permission_desc=Права доступу
team_unit_desc=Дозволити доступ до розділів репозиторію team_unit_desc=Дозволити доступ до розділів репозиторію
team_unit_disabled=(Вимкнено) team_unit_disabled=(Вимкнено)
form.name_been_taken=Назва організації "%s" вже зайнята.
form.name_reserved=Назву організації "%s" зарезервовано. form.name_reserved=Назву організації "%s" зарезервовано.
form.name_pattern_not_allowed=Шаблон "%s" не допускається в назві організації. form.name_pattern_not_allowed=Шаблон "%s" не допускається в назві організації.
form.create_org_not_allowed=Вам не дозволено створювати організації. form.create_org_not_allowed=Вам не дозволено створювати організації.
@ -2608,12 +2641,27 @@ settings.visibility.private_shortname=Приватний
settings.update_settings=Оновити налаштування settings.update_settings=Оновити налаштування
settings.update_setting_success=Налаштування організації оновлені. settings.update_setting_success=Налаштування організації оновлені.
settings.rename=Перейменувати організацію
settings.rename_desc=Зміна назви організації також змінить URL адресу вашої організації і звільнить стару назву.
settings.rename_success=Організацію %[1]s успішно перейменована на %[2].
settings.rename_no_change=Назва організації не змінилася.
settings.rename_new_org_name=Назва нової організації
settings.rename_failed=Не вдалося перейменувати організацію через внутрішню помилку
settings.rename_notices_1=Цю операцію <strong>НЕМОЖЛИВО</strong> скасувати.
settings.rename_notices_2=Стара назва буде перенаправлятися на нову, поки хтось не використає її.
settings.update_avatar_success=Аватар організації оновлений. settings.update_avatar_success=Аватар організації оновлений.
settings.delete=Видалити організацію settings.delete=Видалити організацію
settings.delete_account=Видалити цю організацію settings.delete_account=Видалити цю організацію
settings.delete_prompt=Організацію буде остаточно видалено. Це <strong>НЕМОЖЛИВО</strong> скасувати! settings.delete_prompt=Організацію буде остаточно видалено. Це <strong>НЕМОЖЛИВО</strong> скасувати!
settings.name_confirm=Введіть назву організації для підтвердження:
settings.delete_notices_1=Цю операцію <strong>НЕМОЖЛИВО</strong> скасувати.
settings.delete_notices_2=Ця операція назавжди видалить <strong>сховища</strong> <strong>%s</strong>, включно з кодом, задачами, коментарями, даними вікі та налаштуваннями співавторів.
settings.delete_notices_3=Ця операція назавжди видалить всі <strong>пакети</strong> <strong>%s</strong>.
settings.delete_notices_4=Ця операція назавжди видалить всі <strong>проєкти</strong> <strong>%s</strong>.
settings.confirm_delete_account=Підтвердити видалення settings.confirm_delete_account=Підтвердити видалення
settings.delete_failed=Не вдалося видалити організацію через внутрішню помилку
settings.delete_successful=Організацію <b>%s</b> успішно видалено.
settings.hooks_desc=Додайте веб-хуки, які спрацьовуватимуть для <strong>всіх сховищ</strong> у цій організації. settings.hooks_desc=Додайте веб-хуки, які спрацьовуватимуть для <strong>всіх сховищ</strong> у цій організації.
settings.labels_desc=Додайте мітки, які можна використовувати у задачах для <strong>усіх сховищ</strong> у цій організації. settings.labels_desc=Додайте мітки, які можна використовувати у задачах для <strong>усіх сховищ</strong> у цій організації.
@ -2639,6 +2687,7 @@ teams.leave.detail=Покинути %s?
teams.can_create_org_repo=Створити репозиторії teams.can_create_org_repo=Створити репозиторії
teams.can_create_org_repo_helper=Учасники можуть створювати нові репозиторії в організації. Автор отримає доступ адміністратора до нового репозиторію. teams.can_create_org_repo_helper=Учасники можуть створювати нові репозиторії в організації. Автор отримає доступ адміністратора до нового репозиторію.
teams.none_access=Немає доступу teams.none_access=Немає доступу
teams.none_access_helper=Учасники не можуть переглядати або виконувати будь-які інші дії з цією одиницею. Це не впливає на загальнодоступні сховища.
teams.general_access=Загальний доступ teams.general_access=Загальний доступ
teams.general_access_helper=Дозволи учасників будуть визначатися відповідно до наведеної нижче таблиці дозволів. teams.general_access_helper=Дозволи учасників будуть визначатися відповідно до наведеної нижче таблиці дозволів.
teams.read_access=Читання teams.read_access=Читання
@ -2667,6 +2716,7 @@ teams.remove_all_repos_title=Видалити всі репозиторії ко
teams.remove_all_repos_desc=Це видалить усі репозиторії команди. teams.remove_all_repos_desc=Це видалить усі репозиторії команди.
teams.add_all_repos_title=Додати всі репозиторії teams.add_all_repos_title=Додати всі репозиторії
teams.add_all_repos_desc=Це додасть всі репозиторії організації до команди. teams.add_all_repos_desc=Це додасть всі репозиторії організації до команди.
teams.add_nonexistent_repo=Сховище, яке ви намагаєтеся додати, не існує, будь ласка, створіть його спочатку.
teams.add_duplicate_users=Користувач уже є членом команди. teams.add_duplicate_users=Користувач уже є членом команди.
teams.repos.none=Для команди немає доступних репозиторіїв. teams.repos.none=Для команди немає доступних репозиторіїв.
teams.members.none=Немає членів в цій команді. teams.members.none=Немає членів в цій команді.
@ -2758,6 +2808,7 @@ dashboard.resync_all_hooks=Заново синхронізувати хуки п
dashboard.reinit_missing_repos=Заново ініціалізувати всі відсутні сховища Git'а, для яких існують записи dashboard.reinit_missing_repos=Заново ініціалізувати всі відсутні сховища Git'а, для яких існують записи
dashboard.sync_external_users=Синхронізувати дані зовнішніх користувачів dashboard.sync_external_users=Синхронізувати дані зовнішніх користувачів
dashboard.cleanup_hook_task_table=Очистити таблицю hook_task dashboard.cleanup_hook_task_table=Очистити таблицю hook_task
dashboard.cleanup_packages=Очистити застарілі пакети
dashboard.cleanup_actions=Очищення ресурсів прострочених дій dashboard.cleanup_actions=Очищення ресурсів прострочених дій
dashboard.server_uptime=Час роботи сервера dashboard.server_uptime=Час роботи сервера
dashboard.current_goroutine=Поточна кількість Goroutines dashboard.current_goroutine=Поточна кількість Goroutines
@ -2789,11 +2840,15 @@ dashboard.total_gc_pause=Загальна пауза збирача сміття
dashboard.last_gc_pause=Остання пауза збирача сміття (GC) dashboard.last_gc_pause=Остання пауза збирача сміття (GC)
dashboard.gc_times=Кількість запусків збирача сміття (GC) dashboard.gc_times=Кількість запусків збирача сміття (GC)
dashboard.delete_old_actions=Видалити всі старі дії з бази даних dashboard.delete_old_actions=Видалити всі старі дії з бази даних
dashboard.delete_old_actions.started=Видалення всіх старих дій з бази даних розпочато.
dashboard.update_checker=Перевірка оновлень dashboard.update_checker=Перевірка оновлень
dashboard.delete_old_system_notices=Видалити всі старі системні повідомлення з бази даних
dashboard.gc_lfs=Збір сміття мета-об'єктів LFS dashboard.gc_lfs=Збір сміття мета-об'єктів LFS
dashboard.stop_endless_tasks=Зупинити нескінченні завдання
dashboard.cancel_abandoned_jobs=Скасувати покинуті завдання dashboard.cancel_abandoned_jobs=Скасувати покинуті завдання
dashboard.start_schedule_tasks=Запуск запланованих завдань dashboard.start_schedule_tasks=Запуск запланованих завдань
dashboard.sync_branch.started=Розпочато синхронізацію гілок dashboard.sync_branch.started=Розпочато синхронізацію гілок
dashboard.sync_tag.started=Розпочато синхронізацію міток
dashboard.rebuild_issue_indexer=Перебудувати індексатор задач dashboard.rebuild_issue_indexer=Перебудувати індексатор задач
dashboard.sync_repo_licenses=Синхронізувати ліцензії сховища dashboard.sync_repo_licenses=Синхронізувати ліцензії сховища
@ -3187,6 +3242,7 @@ monitor.queue.settings.remove_all_items_done=Усі елементи черги
notices.system_notice_list=Сповіщення системи notices.system_notice_list=Сповіщення системи
notices.view_detail_header=Переглянути деталі повідомлення notices.view_detail_header=Переглянути деталі повідомлення
notices.operations=Дії
notices.select_all=Вибрати все notices.select_all=Вибрати все
notices.deselect_all=Скасувати виділення notices.deselect_all=Скасувати виділення
notices.inverse_selection=Інвертувати виділене notices.inverse_selection=Інвертувати виділене
@ -3254,6 +3310,7 @@ seconds=%d секунди
minutes=%d хвилини minutes=%d хвилини
hours=%d години hours=%d години
days=%d дні days=%d дні
weeks=%d тижні(в)
months=%d місяці months=%d місяці
years=%d роки years=%d роки
raw_seconds=секунди raw_seconds=секунди
@ -3324,6 +3381,7 @@ details.license=Ліцензія
assets=Ресурси assets=Ресурси
versions=Версії versions=Версії
versions.view_all=Переглянути все versions.view_all=Переглянути все
dependency.id=ID
dependency.version=Версія dependency.version=Версія
search_in_external_registry=Шукати в %s search_in_external_registry=Шукати в %s
alpine.registry=Налаштуйте цей реєстр, додавши URL у ваш файл <code>/etc/apk/repositories</code>: alpine.registry=Налаштуйте цей реєстр, додавши URL у ваш файл <code>/etc/apk/repositories</code>:
@ -3463,6 +3521,8 @@ actions=Дії
unit.desc=Керувати діями unit.desc=Керувати діями
status.unknown=Невідомий status.unknown=Невідомий
status.waiting=Очікування
status.running=Виконується
status.success=Успіх status.success=Успіх
status.failure=Невдача status.failure=Невдача
status.cancelled=Скасовано status.cancelled=Скасовано
@ -3470,6 +3530,7 @@ status.skipped=Пропущено
status.blocked=Заблоковано status.blocked=Заблоковано
runners.status=Статус runners.status=Статус
runners.id=ID
runners.name=Назва runners.name=Назва
runners.owner_type=Тип runners.owner_type=Тип
runners.description=Опис runners.description=Опис
@ -3492,23 +3553,36 @@ runs.all_workflows=Всі робочі процеси
runs.commit=Коміт runs.commit=Коміт
runs.scheduled=Заплановано runs.scheduled=Заплановано
runs.pushed_by=завантажено runs.pushed_by=завантажено
runs.invalid_workflow_helper=Файл конфігурації робочих процесів недійсний. Будь ласка, перевірте файл конфігурації: %s
runs.no_job_without_needs=Робочий процес повинен містити принаймні одну задачу без залежностей.
runs.no_job=Робочий процес повинен містити принаймні одну задачу runs.no_job=Робочий процес повинен містити принаймні одну задачу
runs.actor=Актор runs.actor=Актор
runs.status=Статус runs.status=Статус
runs.actors_no_select=Усі актори runs.actors_no_select=Усі актори
runs.status_no_select=Всі статуси runs.status_no_select=Всі статуси
runs.no_results=Збігів немає. runs.no_results=Збігів немає.
runs.no_workflows=Робочих процесів наразі немає.
runs.no_workflows.quick_start=Не знаєте, як почати з Gitea Дії? Дивіться <a target="_blank" rel="noopener noreferrer" href="%s">посібник швидкого старту</a>.
runs.no_workflows.documentation=Для отримання додаткової інформації про Gitea Дії, перегляньте <a target="_blank" rel="noopener noreferrer" href="%s">документацію</a>. runs.no_workflows.documentation=Для отримання додаткової інформації про Gitea Дії, перегляньте <a target="_blank" rel="noopener noreferrer" href="%s">документацію</a>.
runs.no_runs=Робочий процес ще не виконувався.
runs.empty_commit_message=(порожнє повідомлення коміту) runs.empty_commit_message=(порожнє повідомлення коміту)
runs.expire_log_message=Журнали були очищені, тому що вони були занадто старі.
runs.delete=Видалити запущений робочий процес
runs.delete.description=Ви впевнені, що хочете остаточно видалити цей робочий процес? Цю дію неможливо скасувати.
runs.not_done=Виконання цього робочого процесу не завершено.
runs.view_workflow_file=Перегляд файлу робочого процесу runs.view_workflow_file=Перегляд файлу робочого процесу
workflow.disable=Вимкнути робочий процес workflow.disable=Вимкнути робочий процес
workflow.disable_success=Робочий процес '%s' успішно вимкнено. workflow.disable_success=Робочий процес '%s' успішно вимкнено.
workflow.enable=Увімкнути робочий процес workflow.enable=Увімкнути робочий процес
workflow.enable_success=Робочий процес '%s' успішно ввімкнено. workflow.enable_success=Робочий процес '%s' успішно ввімкнено.
workflow.disabled=Робочий процес вимкнений.
workflow.run=Запустити робочий процес workflow.run=Запустити робочий процес
workflow.not_found=Робочий процес '%s' не знайдено. workflow.not_found=Робочий процес '%s' не знайдено.
workflow.run_success=Робочий процес '%s' завершився успішно.
workflow.from_ref=Використати робочий процес з workflow.from_ref=Використати робочий процес з
workflow.has_workflow_dispatch=Цей робочий процес має тригер події workflow_dispatch.
workflow.has_no_workflow_dispatch=Робочий процес “%s” не має тригера події workflow_dispatch.
variables=Змінні variables=Змінні

View File

@ -1394,6 +1394,7 @@ editor.require_signed_commit=分支需要签名提交
editor.cherry_pick=拣选提交 %s 到: editor.cherry_pick=拣选提交 %s 到:
editor.revert=将 %s 还原到: editor.revert=将 %s 还原到:
commits.desc=浏览代码修改历史 commits.desc=浏览代码修改历史
commits.commits=次代码提交 commits.commits=次代码提交
commits.no_commits=没有共同的提交。「%s」和「%s」的历史完全不同。 commits.no_commits=没有共同的提交。「%s」和「%s」的历史完全不同。

View File

@ -374,6 +374,7 @@ editor.create_new_branch=建立 <strong>新的分支</strong> 為此提交和開
editor.cancel=取消 editor.cancel=取消
editor.no_changes_to_show=沒有可以顯示的變更。 editor.no_changes_to_show=沒有可以顯示的變更。
commits.commits=次程式碼提交 commits.commits=次程式碼提交
commits.author=作者 commits.author=作者
commits.message=備註 commits.message=備註

View File

@ -1356,6 +1356,7 @@ editor.require_signed_commit=分支僅接受經簽署的提交
editor.cherry_pick=Cherry-pick %s 到: editor.cherry_pick=Cherry-pick %s 到:
editor.revert=還原 %s 到: editor.revert=還原 %s 到:
commits.desc=瀏覽原始碼修改歷程。 commits.desc=瀏覽原始碼修改歷程。
commits.commits=次程式碼提交 commits.commits=次程式碼提交
commits.no_commits=沒有共同的提交。「%s」和「%s」的歷史完全不同。 commits.no_commits=沒有共同的提交。「%s」和「%s」的歷史完全不同。

327
routers/web/admin/badges.go Normal file
View File

@ -0,0 +1,327 @@
// Copyright 2024 The Gitea Authors.
// SPDX-License-Identifier: MIT
package admin
import (
"fmt"
"net/http"
"net/url"
"strings"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/web/explore"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
user_service "code.gitea.io/gitea/services/user"
)
const (
tplBadges templates.TplName = "admin/badge/list"
tplBadgeNew templates.TplName = "admin/badge/new"
tplBadgeView templates.TplName = "admin/badge/view"
tplBadgeEdit templates.TplName = "admin/badge/edit"
tplBadgeUsers templates.TplName = "admin/badge/users"
)
// BadgeSearchDefaultAdminSort is the default sort type for admin view
const BadgeSearchDefaultAdminSort = "oldest"
// Badges show all the badges
func Badges(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.badges")
ctx.Data["PageIsAdminBadges"] = true
sortType := ctx.FormString("sort")
if sortType == "" {
sortType = BadgeSearchDefaultAdminSort
ctx.SetFormString("sort", sortType)
}
ctx.PageData["adminBadgeListSearchForm"] = map[string]any{
"SortType": sortType,
}
explore.RenderBadgeSearch(ctx, &user_model.SearchBadgeOptions{
Actor: ctx.Doer,
ListOptions: db.ListOptions{
PageSize: setting.UI.Admin.UserPagingNum,
},
}, tplBadges)
}
// NewBadge render adding a new badge
func NewBadge(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.badges.new_badge")
ctx.Data["PageIsAdminBadges"] = true
ctx.HTML(http.StatusOK, tplBadgeNew)
}
// NewBadgePost response for adding a new badge
func NewBadgePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.AdminCreateBadgeForm)
ctx.Data["Title"] = ctx.Tr("admin.badges.new_badge")
ctx.Data["PageIsAdminBadges"] = true
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplBadgeNew)
return
}
b := &user_model.Badge{
Slug: form.Slug,
Description: form.Description,
ImageURL: form.ImageURL,
}
if len(form.Slug) < 1 {
ctx.Data["Err_Slug"] = true
ctx.RenderWithErr(ctx.Tr("admin.badges.must_fill"), tplBadgeNew, &form)
return
}
if err := user_model.CreateBadge(ctx, b); err != nil {
switch {
default:
ctx.ServerError("CreateBadge", err)
}
return
}
log.Trace("Badge created by admin (%s): %s", ctx.Doer.Name, b.Slug)
ctx.Flash.Success(ctx.Tr("admin.badges.new_success", b.Slug))
ctx.Redirect(setting.AppSubURL + "/-/admin/badges/" + url.PathEscape(b.Slug))
}
func prepareBadgeInfo(ctx *context.Context) *user_model.Badge {
b, err := user_model.GetBadge(ctx, ctx.PathParam(":badge_slug"))
if err != nil {
if user_model.IsErrBadgeNotExist(err) {
ctx.Redirect(setting.AppSubURL + "/-/admin/badges")
} else {
ctx.ServerError("GetBadge", err)
}
return nil
}
ctx.Data["Badge"] = b
opts := &user_model.GetBadgeUsersOptions{
ListOptions: db.ListOptions{
PageSize: setting.UI.User.RepoPagingNum,
},
Badge: b,
}
users, count, err := user_model.GetBadgeUsers(ctx, opts)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Redirect(setting.AppSubURL + "/-/admin/badges")
} else {
ctx.ServerError("GetBadgeUsers", err)
}
return nil
}
ctx.Data["Users"] = users
ctx.Data["UsersTotal"] = int(count)
return b
}
func ViewBadge(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.badges.details")
ctx.Data["PageIsAdminBadges"] = true
prepareBadgeInfo(ctx)
if ctx.Written() {
return
}
ctx.HTML(http.StatusOK, tplBadgeView)
}
// EditBadge show editing badge page
func EditBadge(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.badges.edit_badges")
ctx.Data["PageIsAdminBadges"] = true
prepareBadgeInfo(ctx)
if ctx.Written() {
return
}
ctx.HTML(http.StatusOK, tplBadgeEdit)
}
// EditBadgePost response for editing badge
func EditBadgePost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.badges.edit_badges")
ctx.Data["PageIsAdminBadges"] = true
b := prepareBadgeInfo(ctx)
if ctx.Written() {
return
}
form := web.GetForm(ctx).(*forms.AdminCreateBadgeForm)
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplBadgeEdit)
return
}
if form.Slug != "" {
if err := user_service.UpdateBadge(ctx, ctx.Data["Badge"].(*user_model.Badge)); err != nil {
switch {
default:
ctx.ServerError("UpdateBadge", err)
}
return
}
}
b.ImageURL = form.ImageURL
b.Description = form.Description
if err := user_model.UpdateBadge(ctx, ctx.Data["Badge"].(*user_model.Badge)); err != nil {
ctx.ServerError("UpdateBadge", err)
return
}
log.Trace("Badge updated by admin (%s): %s", ctx.Doer.Name, b.Slug)
ctx.Flash.Success(ctx.Tr("admin.badges.update_success"))
ctx.Redirect(setting.AppSubURL + "/-/admin/badges/" + url.PathEscape(ctx.PathParam(":badge_slug")))
}
// DeleteBadge response for deleting a badge
func DeleteBadge(ctx *context.Context) {
b, err := user_model.GetBadge(ctx, ctx.PathParam(":badge_slug"))
if err != nil {
ctx.ServerError("GetBadge", err)
return
}
if err = user_service.DeleteBadge(ctx, b); err != nil {
ctx.ServerError("DeleteBadge", err)
return
}
log.Trace("Badge deleted by admin (%s): %s", ctx.Doer.Name, b.Slug)
ctx.Flash.Success(ctx.Tr("admin.badges.deletion_success"))
ctx.Redirect(setting.AppSubURL + "/-/admin/badges")
}
func BadgeUsers(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.badges.users_with_badge", ctx.PathParam(":badge_slug"))
ctx.Data["PageIsAdminBadges"] = true
page := ctx.FormInt("page")
if page <= 0 {
page = 1
}
badge := &user_model.Badge{Slug: ctx.PathParam(":badge_slug")}
opts := &user_model.GetBadgeUsersOptions{
ListOptions: db.ListOptions{
Page: page,
PageSize: setting.UI.User.RepoPagingNum,
},
Badge: badge,
}
users, count, err := user_model.GetBadgeUsers(ctx, opts)
if err != nil {
ctx.ServerError("GetBadgeUsers", err)
return
}
ctx.Data["Users"] = users
ctx.Data["Total"] = count
ctx.Data["Page"] = context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5)
ctx.HTML(http.StatusOK, tplBadgeUsers)
}
// BadgeUsersPost response for actions for user badges
func BadgeUsersPost(ctx *context.Context) {
name := strings.ToLower(ctx.FormString("user"))
u, err := user_model.GetUserByName(ctx, name)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
} else {
ctx.ServerError("GetUserByName", err)
}
return
}
if err = user_model.AddUserBadge(ctx, u, &user_model.Badge{Slug: ctx.PathParam(":badge_slug")}); err != nil {
if user_model.IsErrBadgeNotExist(err) {
ctx.Flash.Error(ctx.Tr("admin.badges.not_found"))
} else {
ctx.ServerError("AddUserBadge", err)
}
return
}
ctx.Flash.Success(ctx.Tr("admin.badges.user_add_success"))
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
}
// DeleteBadgeUser delete a badge from a user
func DeleteBadgeUser(ctx *context.Context) {
user, err := user_model.GetUserByID(ctx, ctx.FormInt64("id"))
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
} else {
ctx.ServerError("GetUserByName", err)
return
}
}
if err := user_model.RemoveUserBadge(ctx, user, &user_model.Badge{Slug: ctx.PathParam(":badge_slug")}); err == nil {
ctx.Flash.Success(ctx.Tr("admin.badges.user_remove_success"))
} else {
ctx.Flash.Error("DeleteBadgeUser: " + err.Error())
}
ctx.JSONRedirect(fmt.Sprintf("%s/-/admin/badges/%s/users", setting.AppSubURL, ctx.PathParam(":badge_slug")))
}
// ViewBadgeUsers render badge's users page
func ViewBadgeUsers(ctx *context.Context) {
badge, err := user_model.GetBadge(ctx, ctx.PathParam(":slug"))
if err != nil {
ctx.ServerError("GetBadge", err)
return
}
page := ctx.FormInt("page")
if page <= 0 {
page = 1
}
opts := &user_model.GetBadgeUsersOptions{
ListOptions: db.ListOptions{
Page: page,
PageSize: setting.UI.User.RepoPagingNum,
},
Badge: badge,
}
users, count, err := user_model.GetBadgeUsers(ctx, opts)
if err != nil {
ctx.ServerError("GetBadgeUsers", err)
return
}
ctx.Data["Title"] = badge.Description
ctx.Data["Badge"] = badge
ctx.Data["Users"] = users
ctx.Data["Total"] = count
ctx.Data["Pages"] = context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5)
ctx.HTML(http.StatusOK, tplBadgeUsers)
}

View File

@ -0,0 +1,75 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package explore
import (
"net/http"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/services/context"
)
func RenderBadgeSearch(ctx *context.Context, opts *user_model.SearchBadgeOptions, tplName templates.TplName) {
// Sitemap index for sitemap paths
opts.Page = int(ctx.PathParamInt64("idx"))
if opts.Page <= 1 {
opts.Page = ctx.FormInt("page")
}
if opts.Page <= 1 {
opts.Page = 1
}
var (
badges []*user_model.Badge
count int64
err error
orderBy db.SearchOrderBy
)
// we can not set orderBy to `models.SearchOrderByXxx`, because there may be a JOIN in the statement, different tables may have the same name columns
sortOrder := ctx.FormString("sort")
if sortOrder == "" {
sortOrder = setting.UI.ExploreDefaultSort
}
ctx.Data["SortType"] = sortOrder
switch sortOrder {
case "newest":
orderBy = "`badge`.id DESC"
case "oldest":
orderBy = "`badge`.id ASC"
case "reversealphabetically":
orderBy = "`badge`.slug DESC"
case "alphabetically":
orderBy = "`badge`.slug ASC"
default:
// in case the sortType is not valid, we set it to recent update
ctx.Data["SortType"] = "oldest"
orderBy = "`badge`.id ASC"
}
opts.Keyword = ctx.FormTrim("q")
opts.OrderBy = orderBy
if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
badges, count, err = user_model.SearchBadges(ctx, opts)
if err != nil {
ctx.ServerError("SearchBadges", err)
return
}
}
ctx.Data["Keyword"] = opts.Keyword
ctx.Data["Total"] = count
ctx.Data["Badges"] = badges
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager
ctx.HTML(http.StatusOK, tplName)
}

View File

@ -318,7 +318,7 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
ctx.Data["Page"] = pager ctx.Data["Page"] = pager
ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0 ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0
ctx.Data["AllowDeleteWorkflowRuns"] = ctx.Repo.CanWrite(unit.TypeActions) ctx.Data["CanWriteRepoUnitActions"] = ctx.Repo.CanWrite(unit.TypeActions)
} }
// loadIsRefDeleted loads the IsRefDeleted field for each run in the list. // loadIsRefDeleted loads the IsRefDeleted field for each run in the list.

View File

@ -16,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
@ -39,7 +40,7 @@ const (
editorCommitChoiceNewBranch string = "commit-to-new-branch" editorCommitChoiceNewBranch string = "commit-to-new-branch"
) )
func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) { func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) *context.CommitFormOptions {
cleanedTreePath := files_service.CleanGitTreePath(ctx.Repo.TreePath) cleanedTreePath := files_service.CleanGitTreePath(ctx.Repo.TreePath)
if cleanedTreePath != ctx.Repo.TreePath { if cleanedTreePath != ctx.Repo.TreePath {
redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(cleanedTreePath)) redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(cleanedTreePath))
@ -47,18 +48,28 @@ func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) {
redirectTo += "?" + ctx.Req.URL.RawQuery redirectTo += "?" + ctx.Req.URL.RawQuery
} }
ctx.Redirect(redirectTo) ctx.Redirect(redirectTo)
return return nil
} }
commitFormBehaviors, err := ctx.Repo.PrepareCommitFormBehaviors(ctx, ctx.Doer) commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName)
if err != nil { if err != nil {
ctx.ServerError("PrepareCommitFormBehaviors", err) ctx.ServerError("PrepareCommitFormOptions", err)
return return nil
}
if commitFormOptions.NeedFork {
ForkToEdit(ctx)
return nil
}
if commitFormOptions.WillSubmitToFork && !commitFormOptions.TargetRepo.CanEnableEditor() {
ctx.Data["NotFoundPrompt"] = ctx.Locale.Tr("repo.editor.fork_not_editable")
ctx.NotFound(nil)
} }
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL()
ctx.Data["TreePath"] = ctx.Repo.TreePath ctx.Data["TreePath"] = ctx.Repo.TreePath
ctx.Data["CommitFormBehaviors"] = commitFormBehaviors ctx.Data["CommitFormOptions"] = commitFormOptions
// for online editor // for online editor
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
@ -69,25 +80,27 @@ func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) {
// form fields // form fields
ctx.Data["commit_summary"] = "" ctx.Data["commit_summary"] = ""
ctx.Data["commit_message"] = "" ctx.Data["commit_message"] = ""
ctx.Data["commit_choice"] = util.Iif(commitFormBehaviors.CanCommitToBranch, editorCommitChoiceDirect, editorCommitChoiceNewBranch) ctx.Data["commit_choice"] = util.Iif(commitFormOptions.CanCommitToBranch, editorCommitChoiceDirect, editorCommitChoiceNewBranch)
ctx.Data["new_branch_name"] = getUniquePatchBranchName(ctx, ctx.Doer.LowerName, ctx.Repo.Repository) ctx.Data["new_branch_name"] = getUniquePatchBranchName(ctx, ctx.Doer.LowerName, commitFormOptions.TargetRepo)
ctx.Data["last_commit"] = ctx.Repo.CommitID ctx.Data["last_commit"] = ctx.Repo.CommitID
return commitFormOptions
} }
func prepareTreePathFieldsAndPaths(ctx *context.Context, treePath string) { func prepareTreePathFieldsAndPaths(ctx *context.Context, treePath string) {
// show the tree path fields in the "breadcrumb" and help users to edit the target tree path // show the tree path fields in the "breadcrumb" and help users to edit the target tree path
ctx.Data["TreeNames"], ctx.Data["TreePaths"] = getParentTreeFields(treePath) ctx.Data["TreeNames"], ctx.Data["TreePaths"] = getParentTreeFields(strings.TrimPrefix(treePath, "/"))
} }
type parsedEditorCommitForm[T any] struct { type preparedEditorCommitForm[T any] struct {
form T form T
commonForm *forms.CommitCommonForm commonForm *forms.CommitCommonForm
CommitFormBehaviors *context.CommitFormBehaviors CommitFormOptions *context.CommitFormOptions
TargetBranchName string OldBranchName string
GitCommitter *files_service.IdentityOptions NewBranchName string
GitCommitter *files_service.IdentityOptions
} }
func (f *parsedEditorCommitForm[T]) GetCommitMessage(defaultCommitMessage string) string { func (f *preparedEditorCommitForm[T]) GetCommitMessage(defaultCommitMessage string) string {
commitMessage := util.IfZero(strings.TrimSpace(f.commonForm.CommitSummary), defaultCommitMessage) commitMessage := util.IfZero(strings.TrimSpace(f.commonForm.CommitSummary), defaultCommitMessage)
if body := strings.TrimSpace(f.commonForm.CommitMessage); body != "" { if body := strings.TrimSpace(f.commonForm.CommitMessage); body != "" {
commitMessage += "\n\n" + body commitMessage += "\n\n" + body
@ -95,7 +108,7 @@ func (f *parsedEditorCommitForm[T]) GetCommitMessage(defaultCommitMessage string
return commitMessage return commitMessage
} }
func parseEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *context.Context) *parsedEditorCommitForm[T] { func prepareEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *context.Context) *preparedEditorCommitForm[T] {
form := web.GetForm(ctx).(T) form := web.GetForm(ctx).(T)
if ctx.HasError() { if ctx.HasError() {
ctx.JSONError(ctx.GetErrMsg()) ctx.JSONError(ctx.GetErrMsg())
@ -105,15 +118,22 @@ func parseEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *cont
commonForm := form.GetCommitCommonForm() commonForm := form.GetCommitCommonForm()
commonForm.TreePath = files_service.CleanGitTreePath(commonForm.TreePath) commonForm.TreePath = files_service.CleanGitTreePath(commonForm.TreePath)
commitFormBehaviors, err := ctx.Repo.PrepareCommitFormBehaviors(ctx, ctx.Doer) commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName)
if err != nil { if err != nil {
ctx.ServerError("PrepareCommitFormBehaviors", err) ctx.ServerError("PrepareCommitFormOptions", err)
return nil
}
if commitFormOptions.NeedFork {
// It shouldn't happen, because we should have done the checks in the "GET" request. But just in case.
ctx.JSONError(ctx.Locale.TrString("error.not_found"))
return nil return nil
} }
// check commit behavior // check commit behavior
targetBranchName := util.Iif(commonForm.CommitChoice == editorCommitChoiceNewBranch, commonForm.NewBranchName, ctx.Repo.BranchName) fromBaseBranch := ctx.FormString("from_base_branch")
if targetBranchName == ctx.Repo.BranchName && !commitFormBehaviors.CanCommitToBranch { commitToNewBranch := commonForm.CommitChoice == editorCommitChoiceNewBranch || fromBaseBranch != ""
targetBranchName := util.Iif(commitToNewBranch, commonForm.NewBranchName, ctx.Repo.BranchName)
if targetBranchName == ctx.Repo.BranchName && !commitFormOptions.CanCommitToBranch {
ctx.JSONError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", targetBranchName)) ctx.JSONError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", targetBranchName))
return nil return nil
} }
@ -125,28 +145,63 @@ func parseEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *cont
return nil return nil
} }
return &parsedEditorCommitForm[T]{ if commitToNewBranch {
form: form, // if target branch exists, we should stop
commonForm: commonForm, targetBranchExists, err := git_model.IsBranchExist(ctx, commitFormOptions.TargetRepo.ID, targetBranchName)
CommitFormBehaviors: commitFormBehaviors, if err != nil {
TargetBranchName: targetBranchName, ctx.ServerError("IsBranchExist", err)
GitCommitter: gitCommitter, return nil
} else if targetBranchExists {
if fromBaseBranch != "" {
ctx.JSONError(ctx.Tr("repo.editor.fork_branch_exists", targetBranchName))
} else {
ctx.JSONError(ctx.Tr("repo.editor.branch_already_exists", targetBranchName))
}
return nil
}
}
oldBranchName := ctx.Repo.BranchName
if fromBaseBranch != "" {
err = editorPushBranchToForkedRepository(ctx, ctx.Doer, ctx.Repo.Repository.BaseRepo, fromBaseBranch, commitFormOptions.TargetRepo, targetBranchName)
if err != nil {
log.Error("Unable to editorPushBranchToForkedRepository: %v", err)
ctx.JSONError(ctx.Tr("repo.editor.fork_failed_to_push_branch", targetBranchName))
return nil
}
// we have pushed the base branch as the new branch, now we need to commit the changes directly to the new branch
oldBranchName = targetBranchName
}
return &preparedEditorCommitForm[T]{
form: form,
commonForm: commonForm,
CommitFormOptions: commitFormOptions,
OldBranchName: oldBranchName,
NewBranchName: targetBranchName,
GitCommitter: gitCommitter,
} }
} }
// redirectForCommitChoice redirects after committing the edit to a branch // redirectForCommitChoice redirects after committing the edit to a branch
func redirectForCommitChoice[T any](ctx *context.Context, parsed *parsedEditorCommitForm[T], treePath string) { func redirectForCommitChoice[T any](ctx *context.Context, parsed *preparedEditorCommitForm[T], treePath string) {
// when editing a file in a PR, it should return to the origin location
if returnURI := ctx.FormString("return_uri"); returnURI != "" && httplib.IsCurrentGiteaSiteURL(ctx, returnURI) {
ctx.JSONRedirect(returnURI)
return
}
if parsed.commonForm.CommitChoice == editorCommitChoiceNewBranch { if parsed.commonForm.CommitChoice == editorCommitChoiceNewBranch {
// Redirect to a pull request when possible // Redirect to a pull request when possible
redirectToPullRequest := false redirectToPullRequest := false
repo, baseBranch, headBranch := ctx.Repo.Repository, ctx.Repo.BranchName, parsed.TargetBranchName repo, baseBranch, headBranch := ctx.Repo.Repository, parsed.OldBranchName, parsed.NewBranchName
if repo.UnitEnabled(ctx, unit.TypePullRequests) { if ctx.Repo.Repository.IsFork && parsed.CommitFormOptions.CanCreateBasePullRequest {
redirectToPullRequest = true
} else if parsed.CommitFormBehaviors.CanCreateBasePullRequest {
redirectToPullRequest = true redirectToPullRequest = true
baseBranch = repo.BaseRepo.DefaultBranch baseBranch = repo.BaseRepo.DefaultBranch
headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch
repo = repo.BaseRepo repo = repo.BaseRepo
} else if repo.UnitEnabled(ctx, unit.TypePullRequests) {
redirectToPullRequest = true
} }
if redirectToPullRequest { if redirectToPullRequest {
ctx.JSONRedirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch)) ctx.JSONRedirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch))
@ -154,11 +209,9 @@ func redirectForCommitChoice[T any](ctx *context.Context, parsed *parsedEditorCo
} }
} }
returnURI := ctx.FormString("return_uri") // redirect to the newly updated file
if returnURI == "" || !httplib.IsCurrentGiteaSiteURL(ctx, returnURI) { redirectTo := util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.NewBranchName), util.PathEscapeSegments(treePath))
returnURI = util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.TargetBranchName), util.PathEscapeSegments(treePath)) ctx.JSONRedirect(redirectTo)
}
ctx.JSONRedirect(returnURI)
} }
func editFileOpenExisting(ctx *context.Context) (prefetch []byte, dataRc io.ReadCloser, fInfo *fileInfo) { func editFileOpenExisting(ctx *context.Context) (prefetch []byte, dataRc io.ReadCloser, fInfo *fileInfo) {
@ -268,7 +321,7 @@ func EditFile(ctx *context.Context) {
func EditFilePost(ctx *context.Context) { func EditFilePost(ctx *context.Context) {
editorAction := ctx.PathParam("editor_action") editorAction := ctx.PathParam("editor_action")
isNewFile := editorAction == "_new" isNewFile := editorAction == "_new"
parsed := parseEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx) parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
if ctx.Written() { if ctx.Written() {
return return
} }
@ -292,8 +345,8 @@ func EditFilePost(ctx *context.Context) {
_, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
LastCommitID: parsed.form.LastCommit, LastCommitID: parsed.form.LastCommit,
OldBranch: ctx.Repo.BranchName, OldBranch: parsed.OldBranchName,
NewBranch: parsed.TargetBranchName, NewBranch: parsed.NewBranchName,
Message: parsed.GetCommitMessage(defaultCommitMessage), Message: parsed.GetCommitMessage(defaultCommitMessage),
Files: []*files_service.ChangeRepoFile{ Files: []*files_service.ChangeRepoFile{
{ {
@ -308,7 +361,7 @@ func EditFilePost(ctx *context.Context) {
Committer: parsed.GitCommitter, Committer: parsed.GitCommitter,
}) })
if err != nil { if err != nil {
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err) editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
return return
} }
@ -327,7 +380,7 @@ func DeleteFile(ctx *context.Context) {
// DeleteFilePost response for deleting file // DeleteFilePost response for deleting file
func DeleteFilePost(ctx *context.Context) { func DeleteFilePost(ctx *context.Context) {
parsed := parseEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx) parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
if ctx.Written() { if ctx.Written() {
return return
} }
@ -335,8 +388,8 @@ func DeleteFilePost(ctx *context.Context) {
treePath := ctx.Repo.TreePath treePath := ctx.Repo.TreePath
_, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
LastCommitID: parsed.form.LastCommit, LastCommitID: parsed.form.LastCommit,
OldBranch: ctx.Repo.BranchName, OldBranch: parsed.OldBranchName,
NewBranch: parsed.TargetBranchName, NewBranch: parsed.NewBranchName,
Files: []*files_service.ChangeRepoFile{ Files: []*files_service.ChangeRepoFile{
{ {
Operation: "delete", Operation: "delete",
@ -349,29 +402,29 @@ func DeleteFilePost(ctx *context.Context) {
Committer: parsed.GitCommitter, Committer: parsed.GitCommitter,
}) })
if err != nil { if err != nil {
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err) editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
return return
} }
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath)) ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath))
redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.TargetBranchName, treePath) redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath)
redirectForCommitChoice(ctx, parsed, redirectTreePath) redirectForCommitChoice(ctx, parsed, redirectTreePath)
} }
func UploadFile(ctx *context.Context) { func UploadFile(ctx *context.Context) {
ctx.Data["PageIsUpload"] = true ctx.Data["PageIsUpload"] = true
upload.AddUploadContext(ctx, "repo")
prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath) prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
opts := prepareEditorCommitFormOptions(ctx, "_upload")
prepareEditorCommitFormOptions(ctx, "_upload")
if ctx.Written() { if ctx.Written() {
return return
} }
upload.AddUploadContextForRepo(ctx, opts.TargetRepo)
ctx.HTML(http.StatusOK, tplUploadFile) ctx.HTML(http.StatusOK, tplUploadFile)
} }
func UploadFilePost(ctx *context.Context) { func UploadFilePost(ctx *context.Context) {
parsed := parseEditorCommitSubmittedForm[*forms.UploadRepoFileForm](ctx) parsed := prepareEditorCommitSubmittedForm[*forms.UploadRepoFileForm](ctx)
if ctx.Written() { if ctx.Written() {
return return
} }
@ -379,8 +432,8 @@ func UploadFilePost(ctx *context.Context) {
defaultCommitMessage := ctx.Locale.TrString("repo.editor.upload_files_to_dir", util.IfZero(parsed.form.TreePath, "/")) defaultCommitMessage := ctx.Locale.TrString("repo.editor.upload_files_to_dir", util.IfZero(parsed.form.TreePath, "/"))
err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{ err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{
LastCommitID: parsed.form.LastCommit, LastCommitID: parsed.form.LastCommit,
OldBranch: ctx.Repo.BranchName, OldBranch: parsed.OldBranchName,
NewBranch: parsed.TargetBranchName, NewBranch: parsed.NewBranchName,
TreePath: parsed.form.TreePath, TreePath: parsed.form.TreePath,
Message: parsed.GetCommitMessage(defaultCommitMessage), Message: parsed.GetCommitMessage(defaultCommitMessage),
Files: parsed.form.Files, Files: parsed.form.Files,
@ -389,7 +442,7 @@ func UploadFilePost(ctx *context.Context) {
Committer: parsed.GitCommitter, Committer: parsed.GitCommitter,
}) })
if err != nil { if err != nil {
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err) editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
return return
} }
redirectForCommitChoice(ctx, parsed, parsed.form.TreePath) redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)

View File

@ -25,7 +25,7 @@ func NewDiffPatch(ctx *context.Context) {
// NewDiffPatchPost response for sending patch page // NewDiffPatchPost response for sending patch page
func NewDiffPatchPost(ctx *context.Context) { func NewDiffPatchPost(ctx *context.Context) {
parsed := parseEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx) parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
if ctx.Written() { if ctx.Written() {
return return
} }
@ -33,8 +33,8 @@ func NewDiffPatchPost(ctx *context.Context) {
defaultCommitMessage := ctx.Locale.TrString("repo.editor.patch") defaultCommitMessage := ctx.Locale.TrString("repo.editor.patch")
_, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{ _, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{
LastCommitID: parsed.form.LastCommit, LastCommitID: parsed.form.LastCommit,
OldBranch: ctx.Repo.BranchName, OldBranch: parsed.OldBranchName,
NewBranch: parsed.TargetBranchName, NewBranch: parsed.NewBranchName,
Message: parsed.GetCommitMessage(defaultCommitMessage), Message: parsed.GetCommitMessage(defaultCommitMessage),
Content: strings.ReplaceAll(parsed.form.Content.Value(), "\r\n", "\n"), Content: strings.ReplaceAll(parsed.form.Content.Value(), "\r\n", "\n"),
Author: parsed.GitCommitter, Author: parsed.GitCommitter,
@ -44,7 +44,7 @@ func NewDiffPatchPost(ctx *context.Context) {
err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch") err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
} }
if err != nil { if err != nil {
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err) editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
return return
} }
redirectForCommitChoice(ctx, parsed, parsed.form.TreePath) redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)

View File

@ -45,7 +45,7 @@ func CherryPick(ctx *context.Context) {
func CherryPickPost(ctx *context.Context) { func CherryPickPost(ctx *context.Context) {
fromCommitID := ctx.PathParam("sha") fromCommitID := ctx.PathParam("sha")
parsed := parseEditorCommitSubmittedForm[*forms.CherryPickForm](ctx) parsed := prepareEditorCommitSubmittedForm[*forms.CherryPickForm](ctx)
if ctx.Written() { if ctx.Written() {
return return
} }
@ -53,8 +53,8 @@ func CherryPickPost(ctx *context.Context) {
defaultCommitMessage := util.Iif(parsed.form.Revert, ctx.Locale.TrString("repo.commit.revert-header", fromCommitID), ctx.Locale.TrString("repo.commit.cherry-pick-header", fromCommitID)) defaultCommitMessage := util.Iif(parsed.form.Revert, ctx.Locale.TrString("repo.commit.revert-header", fromCommitID), ctx.Locale.TrString("repo.commit.cherry-pick-header", fromCommitID))
opts := &files.ApplyDiffPatchOptions{ opts := &files.ApplyDiffPatchOptions{
LastCommitID: parsed.form.LastCommit, LastCommitID: parsed.form.LastCommit,
OldBranch: ctx.Repo.BranchName, OldBranch: parsed.OldBranchName,
NewBranch: parsed.TargetBranchName, NewBranch: parsed.NewBranchName,
Message: parsed.GetCommitMessage(defaultCommitMessage), Message: parsed.GetCommitMessage(defaultCommitMessage),
Author: parsed.GitCommitter, Author: parsed.GitCommitter,
Committer: parsed.GitCommitter, Committer: parsed.GitCommitter,
@ -78,7 +78,7 @@ func CherryPickPost(ctx *context.Context) {
} }
} }
if err != nil { if err != nil {
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err) editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
return return
} }
} }

View File

@ -0,0 +1,31 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/services/context"
repo_service "code.gitea.io/gitea/services/repository"
)
const tplEditorFork templates.TplName = "repo/editor/fork"
func ForkToEdit(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplEditorFork)
}
func ForkToEditPost(ctx *context.Context) {
ForkRepoTo(ctx, ctx.Doer, repo_service.ForkRepoOptions{
BaseRepo: ctx.Repo.Repository,
Name: getUniqueRepositoryName(ctx, ctx.Doer.ID, ctx.Repo.Repository.Name),
Description: ctx.Repo.Repository.Description,
SingleBranch: ctx.Repo.Repository.DefaultBranch, // maybe we only need the default branch in the fork?
})
if ctx.Written() {
return
}
ctx.JSONRedirect("") // reload the page, the new fork should be editable now
}

View File

@ -11,9 +11,11 @@ import (
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
repo_module "code.gitea.io/gitea/modules/repository"
context_service "code.gitea.io/gitea/services/context" context_service "code.gitea.io/gitea/services/context"
) )
@ -83,3 +85,26 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
} }
return treeNames, treePaths return treeNames, treePaths
} }
// getUniqueRepositoryName Gets a unique repository name for a user
// It will append a -<num> postfix if the name is already taken
func getUniqueRepositoryName(ctx context.Context, ownerID int64, name string) string {
uniqueName := name
for i := 1; i < 1000; i++ {
_, err := repo_model.GetRepositoryByName(ctx, ownerID, uniqueName)
if err != nil || repo_model.IsErrRepoNotExist(err) {
return uniqueName
}
uniqueName = fmt.Sprintf("%s-%d", name, i)
i++
}
return ""
}
func editorPushBranchToForkedRepository(ctx context.Context, doer *user_model.User, baseRepo *repo_model.Repository, baseBranchName string, targetRepo *repo_model.Repository, targetBranchName string) error {
return git.Push(ctx, baseRepo.RepoPath(), git.PushOptions{
Remote: targetRepo.RepoPath(),
Branch: baseBranchName + ":" + targetBranchName,
Env: repo_module.PushingEnvironment(doer, targetRepo),
})
}

View File

@ -189,17 +189,25 @@ func ForkPost(ctx *context.Context) {
} }
} }
repo, err := repo_service.ForkRepository(ctx, ctx.Doer, ctxUser, repo_service.ForkRepoOptions{ repo := ForkRepoTo(ctx, ctxUser, repo_service.ForkRepoOptions{
BaseRepo: forkRepo, BaseRepo: forkRepo,
Name: form.RepoName, Name: form.RepoName,
Description: form.Description, Description: form.Description,
SingleBranch: form.ForkSingleBranch, SingleBranch: form.ForkSingleBranch,
}) })
if ctx.Written() {
return
}
ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
}
func ForkRepoTo(ctx *context.Context, owner *user_model.User, forkOpts repo_service.ForkRepoOptions) *repo_model.Repository {
repo, err := repo_service.ForkRepository(ctx, ctx.Doer, owner, forkOpts)
if err != nil { if err != nil {
ctx.Data["Err_RepoName"] = true ctx.Data["Err_RepoName"] = true
switch { switch {
case repo_model.IsErrReachLimitOfRepo(err): case repo_model.IsErrReachLimitOfRepo(err):
maxCreationLimit := ctxUser.MaxCreationLimit() maxCreationLimit := owner.MaxCreationLimit()
msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
ctx.JSONError(msg) ctx.JSONError(msg)
case repo_model.IsErrRepoAlreadyExist(err): case repo_model.IsErrRepoAlreadyExist(err):
@ -224,9 +232,7 @@ func ForkPost(ctx *context.Context) {
default: default:
ctx.ServerError("ForkPost", err) ctx.ServerError("ForkPost", err)
} }
return return nil
} }
return repo
log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
} }

View File

@ -290,7 +290,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) { func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) {
// archived or mirror repository, the buttons should not be shown // archived or mirror repository, the buttons should not be shown
if ctx.Repo.Repository.IsArchived || !ctx.Repo.Repository.CanEnableEditor() { if !ctx.Repo.Repository.CanEnableEditor() {
return return
} }
@ -302,7 +302,9 @@ func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) {
} }
if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
ctx.Data["CanEditFile"] = true
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit") ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit")
ctx.Data["CanDeleteFile"] = true
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access") ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
return return
} }

View File

@ -212,7 +212,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil
ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale) ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale)
} }
if !fInfo.isLFSFile && ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { if !fInfo.isLFSFile && ctx.Repo.Repository.CanEnableEditor() {
ctx.Data["CanEditReadmeFile"] = true ctx.Data["CanEditReadmeFile"] = true
} }
} }

View File

@ -749,6 +749,16 @@ func registerWebRoutes(m *web.Router) {
m.Post("/{userid}/avatar/delete", admin.DeleteAvatar) m.Post("/{userid}/avatar/delete", admin.DeleteAvatar)
}) })
m.Group("/badges", func() {
m.Get("", admin.Badges)
m.Combo("/new").Get(admin.NewBadge).Post(web.Bind(forms.AdminCreateBadgeForm{}), admin.NewBadgePost)
m.Get("/{badge_slug}", admin.ViewBadge)
m.Combo("/{badge_slug}/edit").Get(admin.EditBadge).Post(web.Bind(forms.AdminCreateBadgeForm{}), admin.EditBadgePost)
m.Post("/{badge_slug}/delete", admin.DeleteBadge)
m.Combo("/{badge_slug}/users").Get(admin.BadgeUsers).Post(admin.BadgeUsersPost)
m.Post("/{badge_slug}/users/delete", admin.DeleteBadgeUser)
})
m.Group("/emails", func() { m.Group("/emails", func() {
m.Get("", admin.Emails) m.Get("", admin.Emails)
m.Post("/activate", admin.ActivateEmail) m.Post("/activate", admin.ActivateEmail)
@ -1313,23 +1323,35 @@ func registerWebRoutes(m *web.Router) {
}, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived()) }, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived())
// end "/{username}/{reponame}": create or edit issues, pulls, labels, milestones // end "/{username}/{reponame}": create or edit issues, pulls, labels, milestones
m.Group("/{username}/{reponame}", func() { // repo code m.Group("/{username}/{reponame}", func() { // repo code (at least "code reader")
m.Group("", func() { m.Group("", func() {
m.Group("", func() { m.Group("", func() {
m.Post("/_preview/*", repo.DiffPreviewPost) // "GET" requests only need "code reader" permission, "POST" requests need "code writer" permission.
m.Combo("/{editor_action:_edit}/*").Get(repo.EditFile). // Because reader can "fork and edit"
Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost) canWriteToBranch := context.CanWriteToBranch()
m.Combo("/{editor_action:_new}/*").Get(repo.EditFile). m.Post("/_preview/*", repo.DiffPreviewPost) // read-only, fine with "code reader"
Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost) m.Post("/_fork/*", repo.ForkToEditPost) // read-only, fork to own repo, fine with "code reader"
m.Combo("/_delete/*").Get(repo.DeleteFile).
Post(web.Bind(forms.DeleteRepoFileForm{}), repo.DeleteFilePost) // the path params are used in PrepareCommitFormOptions to construct the correct form action URL
m.Combo("/_upload/*", repo.MustBeAbleToUpload).Get(repo.UploadFile). m.Combo("/{editor_action:_edit}/*").
Post(web.Bind(forms.UploadRepoFileForm{}), repo.UploadFilePost) Get(repo.EditFile).
m.Combo("/_diffpatch/*").Get(repo.NewDiffPatch). Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost)
Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost) m.Combo("/{editor_action:_new}/*").
m.Combo("/_cherrypick/{sha:([a-f0-9]{7,64})}/*").Get(repo.CherryPick). Get(repo.EditFile).
Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost) Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost)
}, context.RepoRefByType(git.RefTypeBranch), context.CanWriteToBranch(), repo.WebGitOperationCommonData) m.Combo("/{editor_action:_delete}/*").
Get(repo.DeleteFile).
Post(web.Bind(forms.DeleteRepoFileForm{}), canWriteToBranch, repo.DeleteFilePost)
m.Combo("/{editor_action:_upload}/*", repo.MustBeAbleToUpload).
Get(repo.UploadFile).
Post(web.Bind(forms.UploadRepoFileForm{}), canWriteToBranch, repo.UploadFilePost)
m.Combo("/{editor_action:_diffpatch}/*").
Get(repo.NewDiffPatch).
Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.NewDiffPatchPost)
m.Combo("/{editor_action:_cherrypick}/{sha:([a-f0-9]{7,64})}/*").
Get(repo.CherryPick).
Post(web.Bind(forms.CherryPickForm{}), canWriteToBranch, repo.CherryPickPost)
}, context.RepoRefByType(git.RefTypeBranch), repo.WebGitOperationCommonData)
m.Group("", func() { m.Group("", func() {
m.Post("/upload-file", repo.UploadFileToServer) m.Post("/upload-file", repo.UploadFileToServer)
m.Post("/upload-remove", repo.RemoveUploadFileFromServer) m.Post("/upload-remove", repo.RemoveUploadFileFromServer)

View File

@ -71,11 +71,6 @@ func (r *Repository) CanWriteToBranch(ctx context.Context, user *user_model.User
return issues_model.CanMaintainerWriteToBranch(ctx, r.Permission, branch, user) return issues_model.CanMaintainerWriteToBranch(ctx, r.Permission, branch, user)
} }
// CanEnableEditor returns true if repository is editable and user has proper access level.
func (r *Repository) CanEnableEditor(ctx context.Context, user *user_model.User) bool {
return r.RefFullName.IsBranch() && r.CanWriteToBranch(ctx, user, r.BranchName) && r.Repository.CanEnableEditor() && !r.Repository.IsArchived
}
// CanCreateBranch returns true if repository is editable and user has proper access level. // CanCreateBranch returns true if repository is editable and user has proper access level.
func (r *Repository) CanCreateBranch() bool { func (r *Repository) CanCreateBranch() bool {
return r.Permission.CanWrite(unit_model.TypeCode) && r.Repository.CanCreateBranch() return r.Permission.CanWrite(unit_model.TypeCode) && r.Repository.CanCreateBranch()
@ -94,9 +89,13 @@ func RepoMustNotBeArchived() func(ctx *Context) {
} }
} }
type CommitFormBehaviors struct { type CommitFormOptions struct {
NeedFork bool
TargetRepo *repo_model.Repository
TargetFormAction string
WillSubmitToFork bool
CanCommitToBranch bool CanCommitToBranch bool
EditorEnabled bool
UserCanPush bool UserCanPush bool
RequireSigned bool RequireSigned bool
WillSign bool WillSign bool
@ -106,51 +105,84 @@ type CommitFormBehaviors struct {
CanCreateBasePullRequest bool CanCreateBasePullRequest bool
} }
func (r *Repository) PrepareCommitFormBehaviors(ctx *Context, doer *user_model.User) (*CommitFormBehaviors, error) { func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *repo_model.Repository, doerRepoPerm access_model.Permission, refName git.RefName) (*CommitFormOptions, error) {
protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, r.Repository.ID, r.BranchName) if !refName.IsBranch() {
// it shouldn't happen because middleware already checks
return nil, util.NewInvalidArgumentErrorf("ref %q is not a branch", refName)
}
originRepo := targetRepo
branchName := refName.ShortName()
// TODO: CanMaintainerWriteToBranch is a bad name, but it really does what "CanWriteToBranch" does
if !issues_model.CanMaintainerWriteToBranch(ctx, doerRepoPerm, branchName, doer) {
targetRepo = repo_model.GetForkedRepo(ctx, doer.ID, targetRepo.ID)
if targetRepo == nil {
return &CommitFormOptions{NeedFork: true}, nil
}
// now, we get our own forked repo; it must be writable by us.
}
submitToForkedRepo := targetRepo.ID != originRepo.ID
err := targetRepo.GetBaseRepo(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
userCanPush := true
requireSigned := false
if protectedBranch != nil {
protectedBranch.Repo = r.Repository
userCanPush = protectedBranch.CanUserPush(ctx, doer)
requireSigned = protectedBranch.RequireSignedCommits
}
sign, keyID, _, err := asymkey_service.SignCRUDAction(ctx, r.Repository.RepoPath(), doer, r.Repository.RepoPath(), git.BranchPrefix+r.BranchName) protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, targetRepo.ID, branchName)
canEnableEditor := r.CanEnableEditor(ctx, doer)
canCommit := canEnableEditor && userCanPush
if requireSigned {
canCommit = canCommit && sign
}
wontSignReason := ""
if err != nil { if err != nil {
if asymkey_service.IsErrWontSign(err) { return nil, err
wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason) }
err = nil canPushWithProtection := true
} else { protectionRequireSigned := false
wontSignReason = "error" if protectedBranch != nil {
} protectedBranch.Repo = targetRepo
canPushWithProtection = protectedBranch.CanUserPush(ctx, doer)
protectionRequireSigned = protectedBranch.RequireSignedCommits
} }
canCreateBasePullRequest := ctx.Repo.Repository.BaseRepo != nil && ctx.Repo.Repository.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) willSign, signKeyID, _, err := asymkey_service.SignCRUDAction(ctx, targetRepo.RepoPath(), doer, targetRepo.RepoPath(), refName.String())
canCreatePullRequest := ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypePullRequests) || canCreateBasePullRequest wontSignReason := ""
if asymkey_service.IsErrWontSign(err) {
wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason)
} else if err != nil {
return nil, err
}
return &CommitFormBehaviors{ canCommitToBranch := !submitToForkedRepo /* same repo */ && targetRepo.CanEnableEditor() && canPushWithProtection
CanCommitToBranch: canCommit, if protectionRequireSigned {
EditorEnabled: canEnableEditor, canCommitToBranch = canCommitToBranch && willSign
UserCanPush: userCanPush, }
RequireSigned: requireSigned,
WillSign: sign, canCreateBasePullRequest := targetRepo.BaseRepo != nil && targetRepo.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests)
SigningKey: keyID, canCreatePullRequest := targetRepo.UnitEnabled(ctx, unit_model.TypePullRequests) || canCreateBasePullRequest
opts := &CommitFormOptions{
TargetRepo: targetRepo,
WillSubmitToFork: submitToForkedRepo,
CanCommitToBranch: canCommitToBranch,
UserCanPush: canPushWithProtection,
RequireSigned: protectionRequireSigned,
WillSign: willSign,
SigningKey: signKeyID,
WontSignReason: wontSignReason, WontSignReason: wontSignReason,
CanCreatePullRequest: canCreatePullRequest, CanCreatePullRequest: canCreatePullRequest,
CanCreateBasePullRequest: canCreateBasePullRequest, CanCreateBasePullRequest: canCreateBasePullRequest,
}, err }
editorAction := ctx.PathParam("editor_action")
editorPathParamRemaining := util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
if submitToForkedRepo {
// there is only "default branch" in forked repo, we will use "from_base_branch" to get a new branch from base repo
editorPathParamRemaining = util.PathEscapeSegments(targetRepo.DefaultBranch) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + "?from_base_branch=" + url.QueryEscape(branchName)
}
if editorAction == "_cherrypick" {
opts.TargetFormAction = targetRepo.Link() + "/" + editorAction + "/" + ctx.PathParam("sha") + "/" + editorPathParamRemaining
} else {
opts.TargetFormAction = targetRepo.Link() + "/" + editorAction + "/" + editorPathParamRemaining
}
if ctx.Req.URL.RawQuery != "" {
opts.TargetFormAction += util.Iif(strings.Contains(opts.TargetFormAction, "?"), "&", "?") + ctx.Req.URL.RawQuery
}
return opts, nil
} }
// CanUseTimetracker returns whether a user can use the timetracker. // CanUseTimetracker returns whether a user can use the timetracker.

View File

@ -11,7 +11,9 @@ import (
"regexp" "regexp"
"strings" "strings"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
) )
@ -106,14 +108,17 @@ func AddUploadContext(ctx *context.Context, uploadType string) {
ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Attachment.AllowedTypes, "|", ",") ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Attachment.AllowedTypes, "|", ",")
ctx.Data["UploadMaxFiles"] = setting.Attachment.MaxFiles ctx.Data["UploadMaxFiles"] = setting.Attachment.MaxFiles
ctx.Data["UploadMaxSize"] = setting.Attachment.MaxSize ctx.Data["UploadMaxSize"] = setting.Attachment.MaxSize
case "repo":
ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/upload-file"
ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/upload-remove"
ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/upload-file"
ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Upload.AllowedTypes, "|", ",")
ctx.Data["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles
ctx.Data["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize
default: default:
setting.PanicInDevOrTesting("Invalid upload type: %s", uploadType) setting.PanicInDevOrTesting("Invalid upload type: %s", uploadType)
} }
} }
func AddUploadContextForRepo(ctx reqctx.RequestContext, repo *repo_model.Repository) {
ctxData, repoLink := ctx.GetData(), repo.Link()
ctxData["UploadUrl"] = repoLink + "/upload-file"
ctxData["UploadRemoveUrl"] = repoLink + "/upload-remove"
ctxData["UploadLinkUrl"] = repoLink + "/upload-file"
ctxData["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Upload.AllowedTypes, "|", ",")
ctxData["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles
ctxData["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize
}

View File

@ -25,6 +25,19 @@ type AdminCreateUserForm struct {
Visibility structs.VisibleType Visibility structs.VisibleType
} }
// AdminCreateBadgeForm form for admin to create badge
type AdminCreateBadgeForm struct {
Slug string `binding:"Required;Slug"`
Description string
ImageURL string `binding:"ValidImageUrl"`
}
// Validate validates form fields
func (f *AdminCreateBadgeForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
// Validate validates form fields // Validate validates form fields
func (f *AdminCreateUserForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { func (f *AdminCreateUserForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req) ctx := context.GetValidateContext(req)

View File

@ -38,20 +38,23 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus,
} }
requiredCommitStatuses := make([]*git_model.CommitStatus, 0, len(commitStatuses)) requiredCommitStatuses := make([]*git_model.CommitStatus, 0, len(commitStatuses))
allRequiredContextsMatched := true
for _, gp := range requiredContextsGlob { for _, gp := range requiredContextsGlob {
requiredContextMatched := false
for _, commitStatus := range commitStatuses { for _, commitStatus := range commitStatuses {
if gp.Match(commitStatus.Context) { if gp.Match(commitStatus.Context) {
requiredCommitStatuses = append(requiredCommitStatuses, commitStatus) requiredCommitStatuses = append(requiredCommitStatuses, commitStatus)
break requiredContextMatched = true
} }
} }
allRequiredContextsMatched = allRequiredContextsMatched && requiredContextMatched
} }
if len(requiredCommitStatuses) == 0 { if len(requiredCommitStatuses) == 0 {
return commitstatus.CommitStatusPending return commitstatus.CommitStatusPending
} }
returnedStatus := git_model.CalcCommitStatus(requiredCommitStatuses).State returnedStatus := git_model.CalcCommitStatus(requiredCommitStatuses).State
if len(requiredCommitStatuses) == len(requiredContexts) { if allRequiredContextsMatched {
return returnedStatus return returnedStatus
} }

View File

@ -58,6 +58,15 @@ func TestMergeRequiredContextsCommitStatus(t *testing.T) {
requiredContexts: []string{"Build*", "Build 2t*"}, requiredContexts: []string{"Build*", "Build 2t*"},
expected: commitstatus.CommitStatusFailure, expected: commitstatus.CommitStatusFailure,
}, },
{
commitStatuses: []*git_model.CommitStatus{
{Context: "Build 1", State: commitstatus.CommitStatusSuccess},
{Context: "Build 2", State: commitstatus.CommitStatusSuccess},
{Context: "Build 2t", State: commitstatus.CommitStatusFailure},
},
requiredContexts: []string{"Build*"},
expected: commitstatus.CommitStatusFailure,
},
{ {
commitStatuses: []*git_model.CommitStatus{ commitStatuses: []*git_model.CommitStatus{
{Context: "Build 1", State: commitstatus.CommitStatusSuccess}, {Context: "Build 1", State: commitstatus.CommitStatusSuccess},

58
services/user/badge.go Normal file
View File

@ -0,0 +1,58 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import (
"context"
"fmt"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
)
// UpdateBadgeDescription changes the description and/or image of a badge
func UpdateBadge(ctx context.Context, b *user_model.Badge) error {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
if err := user_model.UpdateBadge(ctx, b); err != nil {
return err
}
return committer.Commit()
}
// DeleteBadge remove record of badge in the database
func DeleteBadge(ctx context.Context, b *user_model.Badge) error {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
if err := user_model.DeleteBadge(ctx, b); err != nil {
return fmt.Errorf("DeleteBadge: %w", err)
}
if err := committer.Commit(); err != nil {
return err
}
_ = committer.Close()
return nil
}
// GetBadgeUsers returns the users that have a specific badge
func GetBadgeUsers(ctx context.Context, badge *user_model.Badge, page, pageSize int) ([]*user_model.User, int64, error) {
opts := &user_model.GetBadgeUsersOptions{
ListOptions: db.ListOptions{
Page: page,
PageSize: pageSize,
},
Badge: badge,
}
return user_model.GetBadgeUsers(ctx, opts)
}

View File

@ -0,0 +1,48 @@
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin edit user")}}
<div class="admin-setting-content">
<h4 class="ui top attached header">
{{ctx.Locale.Tr "admin.badges.edit_badge"}}
</h4>
<div class="ui attached segment">
<form class="ui form" action="./edit" method="post">
{{template "base/disable_form_autofill"}}
{{.CsrfTokenHtml}}
<div class="non-local field {{if .Err_Slug}}error{{end}}" disabled=disabled>
<label for="slug">{{ctx.Locale.Tr "admin.badges.slug"}}</label>
<input disabled=disabled id="slug" name="slug" value="{{.Badge.Slug}}">
</div>
<div class="field {{if .Err_Description}}error{{end}}">
<label for="description">{{ctx.Locale.Tr "admin.badges.description"}}</label>
<textarea id="description" type="text" name="description" rows="2">{{.Badge.Description}}</textarea>
</div>
<div class="field {{if .Err_ImageURL}}error{{end}}">
<label for="image_url">{{ctx.Locale.Tr "admin.badges.image_url"}}</label>
<input id="image_url" type="url" name="image_url" value="{{.Badge.ImageURL}}">
</div>
<div class="divider"></div>
<div class="field">
<button class="ui primary button">{{ctx.Locale.Tr "admin.badges.update_badge"}}</button>
<button class="ui red button show-modal" data-modal="#delete-badge-modal">{{ctx.Locale.Tr "admin.badges.delete_badge"}}</button>
</div>
</form>
</div>
</div>
<div class="ui g-modal-confirm delete modal" id="delete-badge-modal">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "admin.badges.delete_badge"}}
</div>
<form class="ui form" method="post" action="./delete">
<div class="content">
<p>{{ctx.Locale.Tr "admin.badges.delete_badge_desc"}}</p>
{{$.CsrfTokenHtml}}
</div>
{{template "base/modal_actions_confirm" .}}
</form>
</div>
{{template "admin/layout_footer" .}}

View File

@ -0,0 +1,67 @@
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin user")}}
<div class="admin-setting-content">
<h4 class="ui top attached header">
{{ctx.Locale.Tr "admin.badges.badges_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})
<div class="ui right">
<a class="ui primary tiny button" href="{{AppSubUrl}}/-/admin/badges/new">{{ctx.Locale.Tr "admin.badges.new_badge"}}</a>
</div>
</h4>
<div class="ui attached segment">
<form class="ui form ignore-dirty" id="user-list-search-form">
<!-- Right Menu -->
<div class="ui right floated secondary filter menu">
<!-- Sort Menu Item -->
<div class="ui dropdown type jump item">
<span class="text">
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
<button class="item" name="sort" value="oldest">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</button>
<button class="item" name="sort" value="newest">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</button>
<button class="item" name="sort" value="alphabetically">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</button>
<button class="item" name="sort" value="reversealphabetically">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</button>
</div>
</div>
</div>
{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.badge_kind")}}
</form>
</div>
<div class="ui attached table segment">
<table class="ui very basic striped table unstackable">
<thead>
<tr>
<th data-sortt-asc="oldest" data-sortt-desc="newest" data-sortt-default="true">ID{{SortArrow "oldest" "newest" .SortType false}}</th>
<th data-sortt-asc="alphabetically" data-sortt-desc="reversealphabetically">
{{ctx.Locale.Tr "admin.badges.slug"}}
{{SortArrow "alphabetically" "reversealphabeically" $.SortType true}}
</th>
<th>{{ctx.Locale.Tr "admin.badges.description"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .Badges}}
<tr>
<td>{{.ID}}</td>
<td>
<a href="{{$.Link}}/{{.Slug}}">{{.Slug}}</a>
</td>
<td class="gt-ellipsis tw-max-w-48">{{.Description}}</td>
<td>
<div class="tw-flex tw-gap-2">
<a href="{{$.Link}}/{{.Slug}}" data-tooltip-content="{{ctx.Locale.Tr "admin.badges.details"}}">{{svg "octicon-star"}}</a>
<a href="{{$.Link}}/{{.Slug}}/edit" data-tooltip-content="{{ctx.Locale.Tr "edit"}}">{{svg "octicon-pencil"}}</a>
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{template "base/paginate" .}}
</div>
{{template "admin/layout_footer" .}}

View File

@ -0,0 +1,30 @@
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin new user")}}
<div class="admin-setting-content">
<h4 class="ui top attached header">
{{ctx.Locale.Tr "admin.badges.new_badge"}}
</h4>
<div class="ui attached segment">
<form class="ui form" action="{{.Link}}" method="post">
{{template "base/disable_form_autofill"}}
{{.CsrfTokenHtml}}
<div class="required non-local field {{if .Err_Slug}}error{{end}}">
<label for="slug">{{ctx.Locale.Tr "admin.badges.slug"}}</label>
<input autofocus required id="slug" name="slug" value="{{.slug}}">
</div>
<div class="field {{if .Err_Description}}error{{end}}">
<label for="description">{{ctx.Locale.Tr "admin.badges.description"}}</label>
<textarea id="description" type="text" name="description" rows="2">{{.description}}</textarea>
</div>
<div class="field {{if .Err_ImageURL}}error{{end}}">
<label for="image_url">{{ctx.Locale.Tr "admin.badges.image_url"}}</label>
<input id="image_url" type="url" name="image_url" value="{{.image_url}}">
</div>
<div class="field">
<button class="ui primary button">{{ctx.Locale.Tr "admin.badges.new_badge"}}</button>
</div>
</form>
</div>
</div>
{{template "admin/layout_footer" .}}

View File

@ -0,0 +1,55 @@
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin user")}}
<div class="admin-setting-content">
<h4 class="ui top attached header">
{{.Title}}
</h4>
{{if .Users}}
<div class="ui attached segment">
<div class="flex-list">
{{range .Users}}
<div class="flex-item tw-items-center">
<div class="flex-item-leading">
<a href="{{.HomeLink}}">{{ctx.AvatarUtils.Avatar . 32}}</a>
</div>
<div class="flex-item-main">
<div class="flex-item-title">
{{template "shared/user/name" .}}
</div>
</div>
<div class="flex-item-trailing">
<button class="ui red tiny button inline delete-button" data-url="{{$.Link}}/delete" data-id="{{.ID}}">
{{ctx.Locale.Tr "admin.badges.remove_user"}}
</button>
</div>
</div>
{{end}}
</div>
</div>
{{end}}
{{template "base/paginate" .}}
<div class="ui bottom attached segment">
<form class="ui form" id="search-badge-user-form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<div id="search-user-box" class="ui search input tw-align-middle">
<input class="prompt" name="user" placeholder="{{ctx.Locale.Tr "search.user_kind"}}" autocomplete="off" autofocus required>
</div>
<button class="ui primary button">{{ctx.Locale.Tr "admin.badges.add_user"}}</button>
</form>
</div>
</div>
<div class="ui g-modal-confirm delete modal">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "admin.badges.remove_user"}}
</div>
<form class="ui form" method="post" id="remove-badge-user-form" action="{{.Link}}">
<div class="content">
{{$.CsrfTokenHtml}}
<p>{{ctx.Locale.Tr "admin.badges.delete_user_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</form>
</div>
{{template "admin/layout_footer" .}}

View File

@ -0,0 +1,44 @@
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin view user")}}
<div class="admin-setting-content">
<div class="admin-responsive-columns">
<div class="tw-flex-1">
<h4 class="ui top attached header">
{{.Title}}
<div class="ui right">
<a class="ui primary tiny button" href="{{.Link}}/edit">{{ctx.Locale.Tr "admin.users.edit"}}</a>
</div>
</h4>
<div class="ui attached segment">
<div class="flex-list">
<div class="flex-item">
{{if .Image}}
<div class="flex-item-leading">
<img width="64" height="64" src="{{.Badge.ImageURL}}" alt="{{.Badge.Description}}" data-tooltip-content="{{.Badge.Description}}">
</div>
{{end}}
<div class="flex-item-main">
<div class="flex-item-title">
{{.Badge.Slug}}
</div>
<div class="flex-item-body">
{{.Badge.Description}}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<h4 class="ui top attached header">
{{ctx.Locale.Tr "explore.users"}} ({{.UsersTotal}})
<div class="ui right">
<a class="ui primary tiny button" href="{{.Link}}/users">{{ctx.Locale.Tr "admin.badges.manage_users"}}</a>
</div>
</h4>
<div class="ui attached segment">
{{template "explore/user_list" .}}
</div>
</div>
{{template "admin/layout_footer" .}}

View File

@ -13,7 +13,7 @@
</a> </a>
</div> </div>
</details> </details>
<details class="item toggleable-item" {{if or .PageIsAdminUsers .PageIsAdminEmails .PageIsAdminOrganizations .PageIsAdminAuthentications}}open{{end}}> <details class="item toggleable-item" {{if or .PageIsAdminUsers .PageIsAdminBadges .PageIsAdminEmails .PageIsAdminOrganizations .PageIsAdminAuthentications}}open{{end}}>
<summary>{{ctx.Locale.Tr "admin.identity_access"}}</summary> <summary>{{ctx.Locale.Tr "admin.identity_access"}}</summary>
<div class="menu"> <div class="menu">
<a class="{{if .PageIsAdminAuthentications}}active {{end}}item" href="{{AppSubUrl}}/-/admin/auths"> <a class="{{if .PageIsAdminAuthentications}}active {{end}}item" href="{{AppSubUrl}}/-/admin/auths">
@ -25,6 +25,9 @@
<a class="{{if .PageIsAdminUsers}}active {{end}}item" href="{{AppSubUrl}}/-/admin/users"> <a class="{{if .PageIsAdminUsers}}active {{end}}item" href="{{AppSubUrl}}/-/admin/users">
{{ctx.Locale.Tr "admin.users"}} {{ctx.Locale.Tr "admin.users"}}
</a> </a>
<a class="{{if .PageIsAdminBadges}}active {{end}}item" href="{{AppSubUrl}}/-/admin/badges">
{{ctx.Locale.Tr "admin.badges"}}
</a>
<a class="{{if .PageIsAdminEmails}}active {{end}}item" href="{{AppSubUrl}}/-/admin/emails"> <a class="{{if .PageIsAdminEmails}}active {{end}}item" href="{{AppSubUrl}}/-/admin/emails">
{{ctx.Locale.Tr "admin.emails"}} {{ctx.Locale.Tr "admin.emails"}}
</a> </a>

View File

@ -40,7 +40,12 @@
{{svg "octicon-kebab-horizontal"}} {{svg "octicon-kebab-horizontal"}}
<div class="menu flex-items-menu"> <div class="menu flex-items-menu">
<a class="item" href="{{$run.Link}}/workflow">{{svg "octicon-play"}}{{ctx.Locale.Tr "actions.runs.view_workflow_file"}}</a> <a class="item" href="{{$run.Link}}/workflow">{{svg "octicon-play"}}{{ctx.Locale.Tr "actions.runs.view_workflow_file"}}</a>
{{if and $.AllowDeleteWorkflowRuns $run.Status.IsDone}} {{if and $.CanWriteRepoUnitActions (not $run.Status.IsDone)}}
<a class="item link-action" data-url="{{$run.Link}}/cancel">
{{svg "octicon-x"}}{{ctx.Locale.Tr "actions.runs.cancel"}}
</a>
{{end}}
{{if and $.CanWriteRepoUnitActions $run.Status.IsDone}}
<a class="item link-action" <a class="item link-action"
data-url="{{$run.Link}}/delete" data-url="{{$run.Link}}/delete"
data-modal-confirm="{{ctx.Locale.Tr "actions.runs.delete.description"}}" data-modal-confirm="{{ctx.Locale.Tr "actions.runs.delete.description"}}"

View File

@ -4,7 +4,7 @@
data-actions-url="{{.ActionsURL}}" data-actions-url="{{.ActionsURL}}"
data-locale-approve="{{ctx.Locale.Tr "repo.diff.review.approve"}}" data-locale-approve="{{ctx.Locale.Tr "repo.diff.review.approve"}}"
data-locale-cancel="{{ctx.Locale.Tr "cancel"}}" data-locale-cancel="{{ctx.Locale.Tr "actions.runs.cancel"}}"
data-locale-rerun="{{ctx.Locale.Tr "rerun"}}" data-locale-rerun="{{ctx.Locale.Tr "rerun"}}"
data-locale-rerun-all="{{ctx.Locale.Tr "rerun_all"}}" data-locale-rerun-all="{{ctx.Locale.Tr "rerun_all"}}"
data-locale-runs-scheduled="{{ctx.Locale.Tr "actions.runs.scheduled"}}" data-locale-runs-scheduled="{{ctx.Locale.Tr "actions.runs.scheduled"}}"

View File

@ -3,8 +3,9 @@
{{template "repo/header" .}} {{template "repo/header" .}}
<div class="ui container"> <div class="ui container">
{{template "base/alert" .}} {{template "base/alert" .}}
<form class="ui edit form form-fetch-action" method="post" action="{{.RepoLink}}/_cherrypick/{{.FromCommitID}}/{{.BranchName | PathEscapeSegments}}"> <form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}">
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
{{template "repo/editor/common_top" .}}
<input type="hidden" name="revert" value="{{if eq .CherryPickType "revert"}}true{{else}}false{{end}}"> <input type="hidden" name="revert" value="{{if eq .CherryPickType "revert"}}true{{else}}false{{end}}">
<div class="repo-editor-header"> <div class="repo-editor-header">
<div class="breadcrumb"> <div class="breadcrumb">

View File

@ -1,11 +1,11 @@
<div class="commit-form-wrapper"> <div class="commit-form-wrapper">
{{ctx.AvatarUtils.Avatar .SignedUser 40 "commit-avatar"}} {{ctx.AvatarUtils.Avatar .SignedUser 40 "commit-avatar"}}
<div class="commit-form"> <div class="commit-form">
<h3>{{- if .CommitFormBehaviors.WillSign}} <h3>{{- if .CommitFormOptions.WillSign}}
<span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CommitFormBehaviors.SigningKey}}">{{svg "octicon-lock" 24}}</span> <span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CommitFormOptions.SigningKey}}">{{svg "octicon-lock" 24}}</span>
{{ctx.Locale.Tr "repo.editor.commit_signed_changes"}} {{ctx.Locale.Tr "repo.editor.commit_signed_changes"}}
{{- else}} {{- else}}
<span title="{{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .CommitFormBehaviors.WontSignReason)}}">{{svg "octicon-unlock" 24}}</span> <span title="{{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .CommitFormOptions.WontSignReason)}}">{{svg "octicon-unlock" 24}}</span>
{{ctx.Locale.Tr "repo.editor.commit_changes"}} {{ctx.Locale.Tr "repo.editor.commit_changes"}}
{{- end}}</h3> {{- end}}</h3>
<div class="field"> <div class="field">
@ -22,17 +22,17 @@
</div> </div>
<div class="quick-pull-choice js-quick-pull-choice"> <div class="quick-pull-choice js-quick-pull-choice">
<div class="field"> <div class="field">
<div class="ui radio checkbox {{if not .CommitFormBehaviors.CanCommitToBranch}}disabled{{end}}"> <div class="ui radio checkbox {{if not .CommitFormOptions.CanCommitToBranch}}disabled{{end}}">
<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="direct" data-button-text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "direct"}}checked{{end}}> <input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="direct" data-button-text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "direct"}}checked{{end}}>
<label> <label>
{{svg "octicon-git-commit"}} {{svg "octicon-git-commit"}}
{{ctx.Locale.Tr "repo.editor.commit_directly_to_this_branch" .BranchName}} {{ctx.Locale.Tr "repo.editor.commit_directly_to_this_branch" .BranchName}}
{{if not .CommitFormBehaviors.CanCommitToBranch}} {{if not .CommitFormOptions.CanCommitToBranch}}
<div class="ui visible small warning message"> <div class="ui visible small warning message">
{{ctx.Locale.Tr "repo.editor.no_commit_to_branch"}} {{ctx.Locale.Tr "repo.editor.no_commit_to_branch"}}
<ul> <ul>
{{if not .CommitFormBehaviors.UserCanPush}}<li>{{ctx.Locale.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}} {{if not .CommitFormOptions.UserCanPush}}<li>{{ctx.Locale.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}}
{{if and .CommitFormBehaviors.RequireSigned (not .CommitFormBehaviors.WillSign)}}<li>{{ctx.Locale.Tr "repo.editor.require_signed_commit"}}</li>{{end}} {{if and .CommitFormOptions.RequireSigned (not .CommitFormOptions.WillSign)}}<li>{{ctx.Locale.Tr "repo.editor.require_signed_commit"}}</li>{{end}}
</ul> </ul>
</div> </div>
{{end}} {{end}}
@ -42,14 +42,14 @@
{{if and (not .Repository.IsEmpty) (not .IsEditingFileOnly)}} {{if and (not .Repository.IsEmpty) (not .IsEditingFileOnly)}}
<div class="field"> <div class="field">
<div class="ui radio checkbox"> <div class="ui radio checkbox">
{{if .CommitFormBehaviors.CanCreatePullRequest}} {{if .CommitFormOptions.CanCreatePullRequest}}
<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" data-button-text="{{ctx.Locale.Tr "repo.editor.propose_file_change"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}> <input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" data-button-text="{{ctx.Locale.Tr "repo.editor.propose_file_change"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}>
{{else}} {{else}}
<input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" data-button-text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}> <input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" data-button-text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}>
{{end}} {{end}}
<label> <label>
{{svg "octicon-git-pull-request"}} {{svg "octicon-git-pull-request"}}
{{if .CommitFormBehaviors.CanCreatePullRequest}} {{if .CommitFormOptions.CanCreatePullRequest}}
{{ctx.Locale.Tr "repo.editor.create_new_branch"}} {{ctx.Locale.Tr "repo.editor.create_new_branch"}}
{{else}} {{else}}
{{ctx.Locale.Tr "repo.editor.create_new_branch_np"}} {{ctx.Locale.Tr "repo.editor.create_new_branch_np"}}

View File

@ -0,0 +1,6 @@
{{if .CommitFormOptions.WillSubmitToFork}}
<div class="ui blue message">
{{$repoLinkHTML := HTMLFormat `<a href="%s">%s</a>` .CommitFormOptions.TargetRepo.Link .CommitFormOptions.TargetRepo.FullName}}
{{ctx.Locale.Tr "repo.editor.fork_edit_description" $repoLinkHTML}}
</div>
{{end}}

View File

@ -3,8 +3,9 @@
{{template "repo/header" .}} {{template "repo/header" .}}
<div class="ui container"> <div class="ui container">
{{template "base/alert" .}} {{template "base/alert" .}}
<form class="ui form form-fetch-action" method="post"> <form class="ui form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}">
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
{{template "repo/editor/common_top" .}}
{{template "repo/editor/commit_form" .}} {{template "repo/editor/commit_form" .}}
</form> </form>
</div> </div>

View File

@ -3,11 +3,12 @@
{{template "repo/header" .}} {{template "repo/header" .}}
<div class="ui container"> <div class="ui container">
{{template "base/alert" .}} {{template "base/alert" .}}
<form class="ui edit form form-fetch-action" method="post" <form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}"
data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}" data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}"
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}" data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
> >
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
{{template "repo/editor/common_top" .}}
<div class="repo-editor-header"> <div class="repo-editor-header">
{{template "repo/editor/common_breadcrumb" .}} {{template "repo/editor/common_breadcrumb" .}}
</div> </div>

View File

@ -0,0 +1,18 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content repository">
{{template "repo/header" .}}
<div class="ui container">
{{template "base/alert" .}}
<form class="ui form form-fetch-action" method="post" action="{{.RepoLink}}/_fork/{{.BranchName | PathEscapeSegments}}">
{{.CsrfTokenHtml}}
<div class="tw-text-center">
<div class="tw-my-[40px]">
<h3>{{ctx.Locale.Tr "repo.editor.fork_create"}}</h3>
<p>{{ctx.Locale.Tr "repo.editor.fork_create_description"}}</p>
</div>
<button class="ui primary button">{{ctx.Locale.Tr "repo.fork_repo"}}</button>
</div>
</form>
</div>
</div>
{{template "base/footer" .}}

View File

@ -3,11 +3,12 @@
{{template "repo/header" .}} {{template "repo/header" .}}
<div class="ui container"> <div class="ui container">
{{template "base/alert" .}} {{template "base/alert" .}}
<form class="ui edit form form-fetch-action" method="post" action="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}" <form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}"
data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}" data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}"
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}" data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
> >
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
{{template "repo/editor/common_top" .}}
<div class="repo-editor-header"> <div class="repo-editor-header">
<div class="breadcrumb"> <div class="breadcrumb">
{{ctx.Locale.Tr "repo.editor.patching"}} {{ctx.Locale.Tr "repo.editor.patching"}}

View File

@ -3,8 +3,9 @@
{{template "repo/header" .}} {{template "repo/header" .}}
<div class="ui container"> <div class="ui container">
{{template "base/alert" .}} {{template "base/alert" .}}
<form class="ui comment form form-fetch-action" method="post"> <form class="ui comment form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}">
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
{{template "repo/editor/common_top" .}}
<div class="repo-editor-header"> <div class="repo-editor-header">
{{template "repo/editor/common_breadcrumb" .}} {{template "repo/editor/common_breadcrumb" .}}
</div> </div>

View File

@ -41,8 +41,8 @@
<a href="{{.Repository.Link}}/find/{{.RefTypeNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a> <a href="{{.Repository.Link}}/find/{{.RefTypeNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a>
{{end}} {{end}}
{{if and .CanWriteCode .RefFullName.IsBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}} {{if and .RefFullName.IsBranch (not .IsViewFile)}}
<button class="ui dropdown basic compact jump button"{{if not .Repository.CanEnableEditor}} disabled{{end}}> <button class="ui dropdown basic compact jump button repo-add-file" {{if not .Repository.CanEnableEditor}}disabled{{end}}>
{{ctx.Locale.Tr "repo.editor.add_file"}} {{ctx.Locale.Tr "repo.editor.add_file"}}
{{svg "octicon-triangle-down" 14 "dropdown icon"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu"> <div class="menu">

View File

@ -7,6 +7,7 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"maps"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -19,292 +20,278 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestCreateFile(t *testing.T) { func TestEditor(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) { onGiteaRun(t, func(t *testing.T, u *url.URL) {
session := loginUser(t, "user2") sessionUser2 := loginUser(t, "user2")
testCreateFile(t, session, "user2", "repo1", "master", "test.txt", "Content") t.Run("EditFileNotAllowed", testEditFileNotAllowed)
t.Run("DiffPreview", testEditorDiffPreview)
t.Run("CreateFile", testEditorCreateFile)
t.Run("EditFile", func(t *testing.T) {
testEditFile(t, sessionUser2, "user2", "repo1", "master", "README.md", "Hello, World (direct)\n")
testEditFileToNewBranch(t, sessionUser2, "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (commit-to-new-branch)\n")
})
t.Run("PatchFile", testEditorPatchFile)
t.Run("DeleteFile", func(t *testing.T) {
viewLink := "/user2/repo1/src/branch/branch2/README.md"
sessionUser2.MakeRequest(t, NewRequest(t, "GET", viewLink), http.StatusOK)
testEditorActionPostRequest(t, sessionUser2, "/user2/repo1/_delete/branch2/README.md", map[string]string{"commit_choice": "direct"})
sessionUser2.MakeRequest(t, NewRequest(t, "GET", viewLink), http.StatusNotFound)
})
t.Run("ForkToEditFile", func(t *testing.T) {
testForkToEditFile(t, loginUser(t, "user4"), "user4", "user2", "repo1", "master", "README.md")
})
t.Run("WebGitCommitEmail", testEditorWebGitCommitEmail)
t.Run("ProtectedBranch", testEditorProtectedBranch)
}) })
} }
func testEditorCreateFile(t *testing.T) {
session := loginUser(t, "user2")
testCreateFile(t, session, "user2", "repo1", "master", "test.txt", "Content")
testEditorActionPostRequestError(t, session, "/user2/repo1/_new/master/", map[string]string{
"tree_path": "test.txt",
"commit_choice": "direct",
"new_branch_name": "master",
}, `A file named "test.txt" already exists in this repository.`)
testEditorActionPostRequestError(t, session, "/user2/repo1/_new/master/", map[string]string{
"tree_path": "test.txt",
"commit_choice": "commit-to-new-branch",
"new_branch_name": "master",
}, `Branch "master" already exists in this repository.`)
}
func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, filePath, content string) { func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, filePath, content string) {
// Request editor page testEditorActionEdit(t, session, user, repo, "_new", branch, "", map[string]string{
newURL := fmt.Sprintf("/%s/%s/_new/%s/", user, repo, branch)
req := NewRequest(t, "GET", newURL)
resp := session.MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body)
lastCommit := doc.GetInputValueByName("last_commit")
assert.NotEmpty(t, lastCommit)
// Save new file to master branch
req = NewRequestWithValues(t, "POST", newURL, map[string]string{
"_csrf": doc.GetCSRF(),
"last_commit": lastCommit,
"tree_path": filePath, "tree_path": filePath,
"content": content, "content": content,
"commit_choice": "direct", "commit_choice": "direct",
}) })
resp = session.MakeRequest(t, req, http.StatusOK)
assert.NotEmpty(t, test.RedirectURL(resp))
} }
func TestCreateFileOnProtectedBranch(t *testing.T) { func testEditorProtectedBranch(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) { session := loginUser(t, "user2")
session := loginUser(t, "user2") // Change the "master" branch to "protected"
req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"rule_name": "master",
"enable_push": "true",
})
session.MakeRequest(t, req, http.StatusSeeOther)
flashMsg := session.GetCookieFlashMessage()
assert.Equal(t, `Branch protection for rule "master" has been updated.`, flashMsg.SuccessMsg)
csrf := GetUserCSRFToken(t, session) // Try to commit a file to the "master" branch and it should fail
// Change master branch to protected resp := testEditorActionPostRequest(t, session, "/user2/repo1/_new/master/", map[string]string{"tree_path": "test-protected-branch.txt", "commit_choice": "direct"})
req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{ assert.Equal(t, http.StatusBadRequest, resp.Code)
"_csrf": csrf, assert.Equal(t, `Cannot commit to protected branch "master".`, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
"rule_name": "master", }
"enable_push": "true",
})
session.MakeRequest(t, req, http.StatusSeeOther)
// Check if master branch has been locked successfully
flashMsg := session.GetCookieFlashMessage()
assert.Equal(t, `Branch protection for rule "master" has been updated.`, flashMsg.SuccessMsg)
// Request editor page func testEditorActionPostRequest(t *testing.T, session *TestSession, requestPath string, params map[string]string) *httptest.ResponseRecorder {
req = NewRequest(t, "GET", "/user2/repo1/_new/master/") req := NewRequest(t, "GET", requestPath)
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
form := map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"last_commit": htmlDoc.GetInputValueByName("last_commit"),
}
maps.Copy(form, params)
req = NewRequestWithValues(t, "POST", requestPath, form)
return session.MakeRequest(t, req, NoExpectedStatus)
}
func testEditorActionPostRequestError(t *testing.T, session *TestSession, requestPath string, params map[string]string, errorMessage string) {
resp := testEditorActionPostRequest(t, session, requestPath, params)
assert.Equal(t, http.StatusBadRequest, resp.Code)
assert.Equal(t, errorMessage, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
}
func testEditorActionEdit(t *testing.T, session *TestSession, user, repo, editorAction, branch, filePath string, params map[string]string) *httptest.ResponseRecorder {
params["tree_path"] = util.IfZero(params["tree_path"], filePath)
newBranchName := util.Iif(params["commit_choice"] == "direct", branch, params["new_branch_name"])
resp := testEditorActionPostRequest(t, session, fmt.Sprintf("/%s/%s/%s/%s/%s", user, repo, editorAction, branch, filePath), params)
assert.Equal(t, http.StatusOK, resp.Code)
assert.NotEmpty(t, test.RedirectURL(resp))
req := NewRequest(t, "GET", path.Join(user, repo, "raw/branch", newBranchName, params["tree_path"]))
resp = session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, params["content"], resp.Body.String())
return resp
}
func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePath, newContent string) {
testEditorActionEdit(t, session, user, repo, "_edit", branch, filePath, map[string]string{
"content": newContent,
"commit_choice": "direct",
})
}
func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, branch, targetBranch, filePath, newContent string) {
testEditorActionEdit(t, session, user, repo, "_edit", branch, filePath, map[string]string{
"content": newContent,
"commit_choice": "commit-to-new-branch",
"new_branch_name": targetBranch,
})
}
func testEditorDiffPreview(t *testing.T) {
session := loginUser(t, "user2")
req := NewRequestWithValues(t, "POST", "/user2/repo1/_preview/master/README.md", map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"content": "Hello, World (Edited)\n",
})
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), `<span class="added-code">Hello, World (Edited)</span>`)
}
func testEditorPatchFile(t *testing.T) {
session := loginUser(t, "user2")
pathContentCommon := `diff --git a/patch-file-1.txt b/patch-file-1.txt
new file mode 100644
index 0000000000..aaaaaaaaaa
--- /dev/null
+++ b/patch-file-1.txt
@@ -0,0 +1 @@
+`
testEditorActionPostRequest(t, session, "/user2/repo1/_diffpatch/master/", map[string]string{
"content": pathContentCommon + "patched content\n",
"commit_choice": "commit-to-new-branch",
"new_branch_name": "patched-branch",
})
resp := MakeRequest(t, NewRequest(t, "GET", "/user2/repo1/raw/branch/patched-branch/patch-file-1.txt"), http.StatusOK)
assert.Equal(t, "patched content\n", resp.Body.String())
// patch again, it should fail
resp = testEditorActionPostRequest(t, session, "/user2/repo1/_diffpatch/patched-branch/", map[string]string{
"content": pathContentCommon + "another patched content\n",
"commit_choice": "commit-to-new-branch",
"new_branch_name": "patched-branch-1",
})
assert.Equal(t, "Unable to apply patch", test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
}
func testEditorWebGitCommitEmail(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
require.True(t, user.KeepEmailPrivate)
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
gitRepo, _ := git.OpenRepository(git.DefaultContext, repo1.RepoPath())
defer gitRepo.Close()
getLastCommit := func(t *testing.T) *git.Commit {
c, err := gitRepo.GetBranchCommit("master")
require.NoError(t, err)
return c
}
session := loginUser(t, user.Name)
makeReq := func(t *testing.T, link string, params map[string]string, expectedUserName, expectedEmail string) *httptest.ResponseRecorder {
lastCommit := getLastCommit(t)
params["_csrf"] = GetUserCSRFToken(t, session)
params["last_commit"] = lastCommit.ID.String()
params["commit_choice"] = "direct"
req := NewRequestWithValues(t, "POST", link, params)
resp := session.MakeRequest(t, req, NoExpectedStatus)
newCommit := getLastCommit(t)
if expectedUserName == "" {
require.Equal(t, lastCommit.ID.String(), newCommit.ID.String())
respErr := test.ParseJSONError(resp.Body.Bytes())
assert.Equal(t, translation.NewLocale("en-US").TrString("repo.editor.invalid_commit_email"), respErr.ErrorMessage)
} else {
require.NotEqual(t, lastCommit.ID.String(), newCommit.ID.String())
assert.Equal(t, expectedUserName, newCommit.Author.Name)
assert.Equal(t, expectedEmail, newCommit.Author.Email)
assert.Equal(t, expectedUserName, newCommit.Committer.Name)
assert.Equal(t, expectedEmail, newCommit.Committer.Email)
}
return resp
}
uploadFile := func(t *testing.T, name, content string) string {
body := &bytes.Buffer{}
uploadForm := multipart.NewWriter(body)
file, _ := uploadForm.CreateFormFile("file", name)
_, _ = io.Copy(file, strings.NewReader(content))
_ = uploadForm.WriteField("_csrf", GetUserCSRFToken(t, session))
_ = uploadForm.Close()
req := NewRequestWithBody(t, "POST", "/user2/repo1/upload-file", body)
req.Header.Add("Content-Type", uploadForm.FormDataContentType())
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
doc := NewHTMLParser(t, resp.Body) respMap := map[string]string{}
lastCommit := doc.GetInputValueByName("last_commit") DecodeJSON(t, resp, &respMap)
assert.NotEmpty(t, lastCommit) return respMap["uuid"]
}
// Save new file to master branch t.Run("EmailInactive", func(t *testing.T) {
req = NewRequestWithValues(t, "POST", "/user2/repo1/_new/master/", map[string]string{ defer tests.PrintCurrentTest(t)()
"_csrf": doc.GetCSRF(), email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 35, UID: user.ID})
"last_commit": lastCommit, require.False(t, email.IsActivated)
"tree_path": "test.txt", makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{
"content": "Content", "tree_path": "README.md",
"commit_choice": "direct", "content": "test content",
}) "commit_email": email.Email,
}, "", "")
resp = session.MakeRequest(t, req, http.StatusBadRequest)
respErr := test.ParseJSONError(resp.Body.Bytes())
assert.Equal(t, `Cannot commit to protected branch "master".`, respErr.ErrorMessage)
// remove the protected branch
csrf = GetUserCSRFToken(t, session)
// Change master branch to protected
req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/1/delete", map[string]string{
"_csrf": csrf,
})
resp = session.MakeRequest(t, req, http.StatusOK)
res := make(map[string]string)
assert.NoError(t, json.NewDecoder(resp.Body).Decode(&res))
assert.Equal(t, "/user2/repo1/settings/branches", res["redirect"])
// Check if master branch has been locked successfully
flashMsg = session.GetCookieFlashMessage()
assert.Equal(t, `Removing branch protection rule "1" failed.`, flashMsg.ErrorMsg)
}) })
}
func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePath, newContent string) *httptest.ResponseRecorder { t.Run("EmailInvalid", func(t *testing.T) {
// Get to the 'edit this file' page defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", path.Join(user, repo, "_edit", branch, filePath)) email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 1, IsActivated: true})
resp := session.MakeRequest(t, req, http.StatusOK) require.NotEqual(t, email.UID, user.ID)
makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{
htmlDoc := NewHTMLParser(t, resp.Body) "tree_path": "README.md",
lastCommit := htmlDoc.GetInputValueByName("last_commit") "content": "test content",
assert.NotEmpty(t, lastCommit) "commit_email": email.Email,
}, "", "")
// Submit the edits
req = NewRequestWithValues(t, "POST", path.Join(user, repo, "_edit", branch, filePath),
map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"last_commit": lastCommit,
"tree_path": filePath,
"content": newContent,
"commit_choice": "direct",
},
)
resp = session.MakeRequest(t, req, http.StatusOK)
assert.NotEmpty(t, test.RedirectURL(resp))
// Verify the change
req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", branch, filePath))
resp = session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, newContent, resp.Body.String())
return resp
}
func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, branch, targetBranch, filePath, newContent string) *httptest.ResponseRecorder {
// Get to the 'edit this file' page
req := NewRequest(t, "GET", path.Join(user, repo, "_edit", branch, filePath))
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
lastCommit := htmlDoc.GetInputValueByName("last_commit")
assert.NotEmpty(t, lastCommit)
// Submit the edits
req = NewRequestWithValues(t, "POST", path.Join(user, repo, "_edit", branch, filePath),
map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"last_commit": lastCommit,
"tree_path": filePath,
"content": newContent,
"commit_choice": "commit-to-new-branch",
"new_branch_name": targetBranch,
},
)
resp = session.MakeRequest(t, req, http.StatusOK)
assert.NotEmpty(t, test.RedirectURL(resp))
// Verify the change
req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", targetBranch, filePath))
resp = session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, newContent, resp.Body.String())
return resp
}
func TestEditFile(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
session := loginUser(t, "user2")
testEditFile(t, session, "user2", "repo1", "master", "README.md", "Hello, World (Edited)\n")
}) })
}
func TestEditFileToNewBranch(t *testing.T) { testWebGit := func(t *testing.T, linkForKeepPrivate string, paramsForKeepPrivate map[string]string, linkForChosenEmail string, paramsForChosenEmail map[string]string) (resp1, resp2 *httptest.ResponseRecorder) {
onGiteaRun(t, func(t *testing.T, u *url.URL) { t.Run("DefaultEmailKeepPrivate", func(t *testing.T) {
session := loginUser(t, "user2")
testEditFileToNewBranch(t, session, "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited)\n")
})
}
func TestWebGitCommitEmail(t *testing.T) {
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
require.True(t, user.KeepEmailPrivate)
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
gitRepo, _ := git.OpenRepository(git.DefaultContext, repo1.RepoPath())
defer gitRepo.Close()
getLastCommit := func(t *testing.T) *git.Commit {
c, err := gitRepo.GetBranchCommit("master")
require.NoError(t, err)
return c
}
session := loginUser(t, user.Name)
makeReq := func(t *testing.T, link string, params map[string]string, expectedUserName, expectedEmail string) *httptest.ResponseRecorder {
lastCommit := getLastCommit(t)
params["_csrf"] = GetUserCSRFToken(t, session)
params["last_commit"] = lastCommit.ID.String()
params["commit_choice"] = "direct"
req := NewRequestWithValues(t, "POST", link, params)
resp := session.MakeRequest(t, req, NoExpectedStatus)
newCommit := getLastCommit(t)
if expectedUserName == "" {
require.Equal(t, lastCommit.ID.String(), newCommit.ID.String())
respErr := test.ParseJSONError(resp.Body.Bytes())
assert.Equal(t, translation.NewLocale("en-US").TrString("repo.editor.invalid_commit_email"), respErr.ErrorMessage)
} else {
require.NotEqual(t, lastCommit.ID.String(), newCommit.ID.String())
assert.Equal(t, expectedUserName, newCommit.Author.Name)
assert.Equal(t, expectedEmail, newCommit.Author.Email)
assert.Equal(t, expectedUserName, newCommit.Committer.Name)
assert.Equal(t, expectedEmail, newCommit.Committer.Email)
}
return resp
}
uploadFile := func(t *testing.T, name, content string) string {
body := &bytes.Buffer{}
uploadForm := multipart.NewWriter(body)
file, _ := uploadForm.CreateFormFile("file", name)
_, _ = io.Copy(file, strings.NewReader(content))
_ = uploadForm.WriteField("_csrf", GetUserCSRFToken(t, session))
_ = uploadForm.Close()
req := NewRequestWithBody(t, "POST", "/user2/repo1/upload-file", body)
req.Header.Add("Content-Type", uploadForm.FormDataContentType())
resp := session.MakeRequest(t, req, http.StatusOK)
respMap := map[string]string{}
DecodeJSON(t, resp, &respMap)
return respMap["uuid"]
}
t.Run("EmailInactive", func(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 35, UID: user.ID}) paramsForKeepPrivate["commit_email"] = ""
require.False(t, email.IsActivated) resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "user2@noreply.example.org")
makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{
"tree_path": "README.md",
"content": "test content",
"commit_email": email.Email,
}, "", "")
}) })
t.Run("ChooseEmail", func(t *testing.T) {
t.Run("EmailInvalid", func(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()
email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 1, IsActivated: true}) paramsForChosenEmail["commit_email"] = "user2@example.com"
require.NotEqual(t, email.UID, user.ID) resp2 = makeReq(t, linkForChosenEmail, paramsForChosenEmail, "User Two", "user2@example.com")
makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{
"tree_path": "README.md",
"content": "test content",
"commit_email": email.Email,
}, "", "")
}) })
return resp1, resp2
}
testWebGit := func(t *testing.T, linkForKeepPrivate string, paramsForKeepPrivate map[string]string, linkForChosenEmail string, paramsForChosenEmail map[string]string) (resp1, resp2 *httptest.ResponseRecorder) { t.Run("Edit", func(t *testing.T) {
t.Run("DefaultEmailKeepPrivate", func(t *testing.T) { testWebGit(t,
defer tests.PrintCurrentTest(t)() "/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for keep private"},
paramsForKeepPrivate["commit_email"] = "" "/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for chosen email"},
resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "user2@noreply.example.org") )
}) })
t.Run("ChooseEmail", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
paramsForChosenEmail["commit_email"] = "user2@example.com"
resp2 = makeReq(t, linkForChosenEmail, paramsForChosenEmail, "User Two", "user2@example.com")
})
return resp1, resp2
}
t.Run("Edit", func(t *testing.T) { t.Run("UploadDelete", func(t *testing.T) {
testWebGit(t, file1UUID := uploadFile(t, "file1", "File 1")
"/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for keep private"}, file2UUID := uploadFile(t, "file2", "File 2")
"/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for chosen email"}, testWebGit(t,
) "/user2/repo1/_upload/master", map[string]string{"files": file1UUID},
}) "/user2/repo1/_upload/master", map[string]string{"files": file2UUID},
)
testWebGit(t,
"/user2/repo1/_delete/master/file1", map[string]string{},
"/user2/repo1/_delete/master/file2", map[string]string{},
)
})
t.Run("UploadDelete", func(t *testing.T) { t.Run("ApplyPatchCherryPick", func(t *testing.T) {
file1UUID := uploadFile(t, "file1", "File 1") testWebGit(t,
file2UUID := uploadFile(t, "file2", "File 2") "/user2/repo1/_diffpatch/master", map[string]string{
testWebGit(t, "tree_path": "__dummy__",
"/user2/repo1/_upload/master", map[string]string{"files": file1UUID}, "content": `diff --git a/patch-file-1.txt b/patch-file-1.txt
"/user2/repo1/_upload/master", map[string]string{"files": file2UUID},
)
testWebGit(t,
"/user2/repo1/_delete/master/file1", map[string]string{},
"/user2/repo1/_delete/master/file2", map[string]string{},
)
})
t.Run("ApplyPatchCherryPick", func(t *testing.T) {
testWebGit(t,
"/user2/repo1/_diffpatch/master", map[string]string{
"tree_path": "__dummy__",
"content": `diff --git a/patch-file-1.txt b/patch-file-1.txt
new file mode 100644 new file mode 100644
index 0000000000..aaaaaaaaaa index 0000000000..aaaaaaaaaa
--- /dev/null --- /dev/null
@ -312,10 +299,10 @@ index 0000000000..aaaaaaaaaa
@@ -0,0 +1 @@ @@ -0,0 +1 @@
+File 1 +File 1
`, `,
}, },
"/user2/repo1/_diffpatch/master", map[string]string{ "/user2/repo1/_diffpatch/master", map[string]string{
"tree_path": "__dummy__", "tree_path": "__dummy__",
"content": `diff --git a/patch-file-2.txt b/patch-file-2.txt "content": `diff --git a/patch-file-2.txt b/patch-file-2.txt
new file mode 100644 new file mode 100644
index 0000000000..bbbbbbbbbb index 0000000000..bbbbbbbbbb
--- /dev/null --- /dev/null
@ -323,20 +310,146 @@ index 0000000000..bbbbbbbbbb
@@ -0,0 +1 @@ @@ -0,0 +1 @@
+File 2 +File 2
`, `,
}, },
) )
commit1, err := gitRepo.GetCommitByPath("patch-file-1.txt") commit1, err := gitRepo.GetCommitByPath("patch-file-1.txt")
require.NoError(t, err) require.NoError(t, err)
commit2, err := gitRepo.GetCommitByPath("patch-file-2.txt") commit2, err := gitRepo.GetCommitByPath("patch-file-2.txt")
require.NoError(t, err) require.NoError(t, err)
resp1, _ := testWebGit(t, resp1, _ := testWebGit(t,
"/user2/repo1/_cherrypick/"+commit1.ID.String()+"/master", map[string]string{"revert": "true"}, "/user2/repo1/_cherrypick/"+commit1.ID.String()+"/master", map[string]string{"revert": "true"},
"/user2/repo1/_cherrypick/"+commit2.ID.String()+"/master", map[string]string{"revert": "true"}, "/user2/repo1/_cherrypick/"+commit2.ID.String()+"/master", map[string]string{"revert": "true"},
) )
// By the way, test the "cherrypick" page: a successful revert redirects to the main branch // By the way, test the "cherrypick" page: a successful revert redirects to the main branch
assert.Equal(t, "/user2/repo1/src/branch/master", test.RedirectURL(resp1)) assert.Equal(t, "/user2/repo1/src/branch/master", test.RedirectURL(resp1))
})
}) })
} }
func testForkToEditFile(t *testing.T, session *TestSession, user, owner, repo, branch, filePath string) {
forkToEdit := func(t *testing.T, session *TestSession, owner, repo, operation, branch, filePath string) {
// visit the base repo, see the "Add File" button
req := NewRequest(t, "GET", path.Join(owner, repo))
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
AssertHTMLElement(t, htmlDoc, ".repo-add-file", 1)
// attempt to edit a file, see the guideline page
req = NewRequest(t, "GET", path.Join(owner, repo, operation, branch, filePath))
resp = session.MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), "Fork Repository to Propose Changes")
// fork the repository
req = NewRequestWithValues(t, "POST", path.Join(owner, repo, "_fork", branch), map[string]string{"_csrf": GetUserCSRFToken(t, session)})
resp = session.MakeRequest(t, req, http.StatusOK)
assert.JSONEq(t, `{"redirect":""}`, resp.Body.String())
}
t.Run("ForkButArchived", func(t *testing.T) {
// Fork repository because we can't edit it
forkToEdit(t, session, owner, repo, "_edit", branch, filePath)
// Archive the repository
req := NewRequestWithValues(t, "POST", path.Join(user, repo, "settings"),
map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"repo_name": repo,
"action": "archive",
},
)
session.MakeRequest(t, req, http.StatusSeeOther)
// Check editing archived repository is disabled
req = NewRequest(t, "GET", path.Join(owner, repo, "_edit", branch, filePath)).SetHeader("Accept", "text/html")
resp := session.MakeRequest(t, req, http.StatusNotFound)
assert.Contains(t, resp.Body.String(), "You have forked this repository but your fork is not editable.")
// Unfork the repository
req = NewRequestWithValues(t, "POST", path.Join(user, repo, "settings"),
map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"repo_name": repo,
"action": "convert_fork",
},
)
session.MakeRequest(t, req, http.StatusSeeOther)
})
// Fork repository again, and check the existence of the forked repo with unique name
forkToEdit(t, session, owner, repo, "_edit", branch, filePath)
session.MakeRequest(t, NewRequestf(t, "GET", "/%s/%s-1", user, repo), http.StatusOK)
t.Run("CheckBaseRepoForm", func(t *testing.T) {
// the base repo's edit form should have the correct action and upload links (pointing to the forked repo)
req := NewRequest(t, "GET", path.Join(owner, repo, "_upload", branch, filePath)+"?foo=bar")
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
uploadForm := htmlDoc.doc.Find(".form-fetch-action")
formAction := uploadForm.AttrOr("action", "")
assert.Equal(t, fmt.Sprintf("/%s/%s-1/_upload/%s/%s?from_base_branch=%s&foo=bar", user, repo, branch, filePath, branch), formAction)
uploadLink := uploadForm.Find(".dropzone").AttrOr("data-link-url", "")
assert.Equal(t, fmt.Sprintf("/%s/%s-1/upload-file", user, repo), uploadLink)
newBranchName := uploadForm.Find("input[name=new_branch_name]").AttrOr("value", "")
assert.Equal(t, user+"-patch-1", newBranchName)
commitChoice := uploadForm.Find("input[name=commit_choice][checked]").AttrOr("value", "")
assert.Equal(t, "commit-to-new-branch", commitChoice)
lastCommit := uploadForm.Find("input[name=last_commit]").AttrOr("value", "")
assert.NotEmpty(t, lastCommit)
})
t.Run("ViewBaseEditFormAndCommitToFork", func(t *testing.T) {
req := NewRequest(t, "GET", path.Join(owner, repo, "_edit", branch, filePath))
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
editRequestForm := map[string]string{
"_csrf": GetUserCSRFToken(t, session),
"last_commit": htmlDoc.GetInputValueByName("last_commit"),
"tree_path": filePath,
"content": "new content in fork",
"commit_choice": "commit-to-new-branch",
}
// change a file in the forked repo with existing branch name (should fail)
editRequestForm["new_branch_name"] = "master"
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s-1/_edit/%s/%s?from_base_branch=%s", user, repo, branch, filePath, branch), editRequestForm)
resp = session.MakeRequest(t, req, http.StatusBadRequest)
respJSON := test.ParseJSONError(resp.Body.Bytes())
assert.Equal(t, `Branch "master" already exists in your fork, please choose a new branch name.`, respJSON.ErrorMessage)
// change a file in the forked repo (should succeed)
newBranchName := htmlDoc.GetInputValueByName("new_branch_name")
editRequestForm["new_branch_name"] = newBranchName
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s-1/_edit/%s/%s?from_base_branch=%s", user, repo, branch, filePath, branch), editRequestForm)
resp = session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, fmt.Sprintf("/%s/%s/compare/%s...%s/%s-1:%s", owner, repo, branch, user, repo, newBranchName), test.RedirectURL(resp))
// check the file in the fork's branch is changed
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s-1/src/branch/%s/%s", user, repo, newBranchName, filePath))
resp = session.MakeRequest(t, req, http.StatusOK)
assert.Contains(t, resp.Body.String(), "new content in fork")
})
}
func testEditFileNotAllowed(t *testing.T) {
sessionUser1 := loginUser(t, "user1") // admin, all access
sessionUser4 := loginUser(t, "user4")
// "_cherrypick" has a different route pattern, so skip its test
operations := []string{"_new", "_edit", "_delete", "_upload", "_diffpatch"}
for _, operation := range operations {
t.Run(operation, func(t *testing.T) {
// Branch does not exist
targetLink := path.Join("user2", "repo1", operation, "missing", "README.md")
sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound)
// Private repository
targetLink = path.Join("user2", "repo2", operation, "master", "Home.md")
sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusOK)
sessionUser4.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound)
// Empty repository
targetLink = path.Join("org41", "repo61", operation, "master", "README.md")
sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound)
})
}
}

View File

@ -42,7 +42,7 @@ func (doc *HTMLDoc) GetCSRF() string {
return doc.GetInputValueByName("_csrf") return doc.GetInputValueByName("_csrf")
} }
// AssertHTMLElement check if element by selector exists or does not exist depending on checkExists // AssertHTMLElement check if the element by selector exists or does not exist depending on checkExists
func AssertHTMLElement[T int | bool](t testing.TB, doc *HTMLDoc, selector string, checkExists T) { func AssertHTMLElement[T int | bool](t testing.TB, doc *HTMLDoc, selector string, checkExists T) {
sel := doc.doc.Find(selector) sel := doc.doc.Find(selector)
switch v := any(checkExists).(type) { switch v := any(checkExists).(type) {

View File

@ -129,7 +129,7 @@ func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) {
session := loginUser(t, "user1") session := loginUser(t, "user1")
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1")
testEditFileToNewBranch(t, session, "user1", "repo1", "status1", "status1", "README.md", "# repo1\n\nDescription for repo1") testEditFile(t, session, "user1", "repo1", "status1", "README.md", "# repo1\n\nDescription for repo1")
url := path.Join("user1", "repo1", "compare", "master...status1") url := path.Join("user1", "repo1", "compare", "master...status1")
req := NewRequestWithValues(t, "POST", url, req := NewRequestWithValues(t, "POST", url,

View File

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
// TODO: Switch to upstream after https://github.com/razorness/vue3-calendar-heatmap/pull/34 is merged // TODO: Switch to upstream after https://github.com/razorness/vue3-calendar-heatmap/pull/34 is merged
import {CalendarHeatmap} from '@silverwind/vue3-calendar-heatmap'; import {CalendarHeatmap} from '@silverwind/vue3-calendar-heatmap';
import {onMounted, ref} from 'vue'; import {onMounted, shallowRef} from 'vue';
import type {Value as HeatmapValue, Locale as HeatmapLocale} from '@silverwind/vue3-calendar-heatmap'; import type {Value as HeatmapValue, Locale as HeatmapLocale} from '@silverwind/vue3-calendar-heatmap';
defineProps<{ defineProps<{
@ -24,7 +24,7 @@ const colorRange = [
'var(--color-primary-dark-4)', 'var(--color-primary-dark-4)',
]; ];
const endDate = ref(new Date()); const endDate = shallowRef(new Date());
onMounted(() => { onMounted(() => {
// work around issue with first legend color being rendered twice and legend cut off // work around issue with first legend color being rendered twice and legend cut off

View File

@ -2,16 +2,16 @@
import {SvgIcon} from '../svg.ts'; import {SvgIcon} from '../svg.ts';
import {GET} from '../modules/fetch.ts'; import {GET} from '../modules/fetch.ts';
import {getIssueColor, getIssueIcon} from '../features/issue.ts'; import {getIssueColor, getIssueIcon} from '../features/issue.ts';
import {computed, onMounted, ref} from 'vue'; import {computed, onMounted, shallowRef, useTemplateRef} from 'vue';
import type {IssuePathInfo} from '../types.ts'; import type {IssuePathInfo} from '../types.ts';
const {appSubUrl, i18n} = window.config; const {appSubUrl, i18n} = window.config;
const loading = ref(false); const loading = shallowRef(false);
const issue = ref(null); const issue = shallowRef(null);
const renderedLabels = ref(''); const renderedLabels = shallowRef('');
const i18nErrorOccurred = i18n.error_occurred; const i18nErrorOccurred = i18n.error_occurred;
const i18nErrorMessage = ref(null); const i18nErrorMessage = shallowRef(null);
const createdAt = computed(() => new Date(issue.value.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'})); const createdAt = computed(() => new Date(issue.value.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'}));
const body = computed(() => { const body = computed(() => {
@ -22,7 +22,7 @@ const body = computed(() => {
return body; return body;
}); });
const root = ref<HTMLElement | null>(null); const root = useTemplateRef('root');
onMounted(() => { onMounted(() => {
root.value.addEventListener('ce-load-context-popup', (e: CustomEventInit<IssuePathInfo>) => { root.value.addEventListener('ce-load-context-popup', (e: CustomEventInit<IssuePathInfo>) => {

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import {SvgIcon, type SvgName} from '../svg.ts'; import {SvgIcon, type SvgName} from '../svg.ts';
import {ref} from 'vue'; import {shallowRef} from 'vue';
import {type DiffStatus, type DiffTreeEntry, diffTreeStore} from '../modules/diff-file.ts'; import {type DiffStatus, type DiffTreeEntry, diffTreeStore} from '../modules/diff-file.ts';
const props = defineProps<{ const props = defineProps<{
@ -8,7 +8,7 @@ const props = defineProps<{
}>(); }>();
const store = diffTreeStore(); const store = diffTreeStore();
const collapsed = ref(props.item.IsViewed); const collapsed = shallowRef(props.item.IsViewed);
function getIconForDiffStatus(pType: DiffStatus) { function getIconForDiffStatus(pType: DiffStatus) {
const diffTypes: Record<DiffStatus, { name: SvgName, classes: Array<string> }> = { const diffTypes: Record<DiffStatus, { name: SvgName, classes: Array<string> }> = {

View File

@ -1,19 +1,19 @@
<script lang="ts" setup> <script lang="ts" setup>
import {computed, onMounted, onUnmounted, ref, watch} from 'vue'; import {computed, onMounted, onUnmounted, shallowRef, watch} from 'vue';
import {SvgIcon} from '../svg.ts'; import {SvgIcon} from '../svg.ts';
import {toggleElem} from '../utils/dom.ts'; import {toggleElem} from '../utils/dom.ts';
const {csrfToken, pageData} = window.config; const {csrfToken, pageData} = window.config;
const mergeForm = ref(pageData.pullRequestMergeForm); const mergeForm = pageData.pullRequestMergeForm;
const mergeTitleFieldValue = ref(''); const mergeTitleFieldValue = shallowRef('');
const mergeMessageFieldValue = ref(''); const mergeMessageFieldValue = shallowRef('');
const deleteBranchAfterMerge = ref(false); const deleteBranchAfterMerge = shallowRef(false);
const autoMergeWhenSucceed = ref(false); const autoMergeWhenSucceed = shallowRef(false);
const mergeStyle = ref(''); const mergeStyle = shallowRef('');
const mergeStyleDetail = ref({ const mergeStyleDetail = shallowRef({
hideMergeMessageTexts: false, hideMergeMessageTexts: false,
textDoMerge: '', textDoMerge: '',
mergeTitleFieldText: '', mergeTitleFieldText: '',
@ -21,33 +21,33 @@ const mergeStyleDetail = ref({
hideAutoMerge: false, hideAutoMerge: false,
}); });
const mergeStyleAllowedCount = ref(0); const mergeStyleAllowedCount = shallowRef(0);
const showMergeStyleMenu = ref(false); const showMergeStyleMenu = shallowRef(false);
const showActionForm = ref(false); const showActionForm = shallowRef(false);
const mergeButtonStyleClass = computed(() => { const mergeButtonStyleClass = computed(() => {
if (mergeForm.value.allOverridableChecksOk) return 'primary'; if (mergeForm.allOverridableChecksOk) return 'primary';
return autoMergeWhenSucceed.value ? 'primary' : 'red'; return autoMergeWhenSucceed.value ? 'primary' : 'red';
}); });
const forceMerge = computed(() => { const forceMerge = computed(() => {
return mergeForm.value.canMergeNow && !mergeForm.value.allOverridableChecksOk; return mergeForm.canMergeNow && !mergeForm.allOverridableChecksOk;
}); });
watch(mergeStyle, (val) => { watch(mergeStyle, (val) => {
mergeStyleDetail.value = mergeForm.value.mergeStyles.find((e: any) => e.name === val); mergeStyleDetail.value = mergeForm.mergeStyles.find((e: any) => e.name === val);
for (const elem of document.querySelectorAll('[data-pull-merge-style]')) { for (const elem of document.querySelectorAll('[data-pull-merge-style]')) {
toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val); toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val);
} }
}); });
onMounted(() => { onMounted(() => {
mergeStyleAllowedCount.value = mergeForm.value.mergeStyles.reduce((v: any, msd: any) => v + (msd.allowed ? 1 : 0), 0); mergeStyleAllowedCount.value = mergeForm.mergeStyles.reduce((v: any, msd: any) => v + (msd.allowed ? 1 : 0), 0);
let mergeStyle = mergeForm.value.mergeStyles.find((e: any) => e.allowed && e.name === mergeForm.value.defaultMergeStyle)?.name; let mergeStyle = mergeForm.mergeStyles.find((e: any) => e.allowed && e.name === mergeForm.defaultMergeStyle)?.name;
if (!mergeStyle) mergeStyle = mergeForm.value.mergeStyles.find((e: any) => e.allowed)?.name; if (!mergeStyle) mergeStyle = mergeForm.mergeStyles.find((e: any) => e.allowed)?.name;
switchMergeStyle(mergeStyle, !mergeForm.value.canMergeNow); switchMergeStyle(mergeStyle, !mergeForm.canMergeNow);
document.addEventListener('mouseup', hideMergeStyleMenu); document.addEventListener('mouseup', hideMergeStyleMenu);
}); });
@ -63,7 +63,7 @@ function hideMergeStyleMenu() {
function toggleActionForm(show: boolean) { function toggleActionForm(show: boolean) {
showActionForm.value = show; showActionForm.value = show;
if (!show) return; if (!show) return;
deleteBranchAfterMerge.value = mergeForm.value.defaultDeleteBranchAfterMerge; deleteBranchAfterMerge.value = mergeForm.defaultDeleteBranchAfterMerge;
mergeTitleFieldValue.value = mergeStyleDetail.value.mergeTitleFieldText; mergeTitleFieldValue.value = mergeStyleDetail.value.mergeTitleFieldText;
mergeMessageFieldValue.value = mergeStyleDetail.value.mergeMessageFieldText; mergeMessageFieldValue.value = mergeStyleDetail.value.mergeMessageFieldText;
} }
@ -74,7 +74,7 @@ function switchMergeStyle(name: string, autoMerge = false) {
} }
function clearMergeMessage() { function clearMergeMessage() {
mergeMessageFieldValue.value = mergeForm.value.defaultMergeMessage; mergeMessageFieldValue.value = mergeForm.defaultMergeMessage;
} }
</script> </script>

View File

@ -1,9 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
// @ts-expect-error - module exports no types // @ts-expect-error - module exports no types
import {VueBarGraph} from 'vue-bar-graph'; import {VueBarGraph} from 'vue-bar-graph';
import {computed, onMounted, ref} from 'vue'; import {computed, onMounted, shallowRef, useTemplateRef} from 'vue';
const colors = ref({ const colors = shallowRef({
barColor: 'green', barColor: 'green',
textColor: 'black', textColor: 'black',
textAltColor: 'white', textAltColor: 'white',
@ -41,8 +41,8 @@ const graphWidth = computed(() => {
return activityTopAuthors.length * 40; return activityTopAuthors.length * 40;
}); });
const styleElement = ref<HTMLElement | null>(null); const styleElement = useTemplateRef('styleElement');
const altStyleElement = ref<HTMLElement | null>(null); const altStyleElement = useTemplateRef('altStyleElement');
onMounted(() => { onMounted(() => {
const refStyle = window.getComputedStyle(styleElement.value); const refStyle = window.getComputedStyle(styleElement.value);

View File

@ -23,7 +23,7 @@ import {
import {chartJsColors} from '../utils/color.ts'; import {chartJsColors} from '../utils/color.ts';
import {sleep} from '../utils.ts'; import {sleep} from '../utils.ts';
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm'; import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
import {onMounted, ref} from 'vue'; import {onMounted, shallowRef} from 'vue';
const {pageData} = window.config; const {pageData} = window.config;
@ -47,10 +47,10 @@ defineProps<{
}; };
}>(); }>();
const isLoading = ref(false); const isLoading = shallowRef(false);
const errorText = ref(''); const errorText = shallowRef('');
const repoLink = ref(pageData.repoLink || []); const repoLink = pageData.repoLink;
const data = ref<DayData[]>([]); const data = shallowRef<DayData[]>([]);
onMounted(() => { onMounted(() => {
fetchGraphData(); fetchGraphData();
@ -61,7 +61,7 @@ async function fetchGraphData() {
try { try {
let response: Response; let response: Response;
do { do {
response = await GET(`${repoLink.value}/activity/code-frequency/data`); response = await GET(`${repoLink}/activity/code-frequency/data`);
if (response.status === 202) { if (response.status === 202) {
await sleep(1000); // wait for 1 second before retrying await sleep(1000); // wait for 1 second before retrying
} }

View File

@ -21,7 +21,7 @@ import {
import {chartJsColors} from '../utils/color.ts'; import {chartJsColors} from '../utils/color.ts';
import {sleep} from '../utils.ts'; import {sleep} from '../utils.ts';
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm'; import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
import {onMounted, ref} from 'vue'; import {onMounted, ref, shallowRef} from 'vue';
const {pageData} = window.config; const {pageData} = window.config;
@ -43,9 +43,9 @@ defineProps<{
}; };
}>(); }>();
const isLoading = ref(false); const isLoading = shallowRef(false);
const errorText = ref(''); const errorText = shallowRef('');
const repoLink = ref(pageData.repoLink || []); const repoLink = pageData.repoLink;
const data = ref<DayData[]>([]); const data = ref<DayData[]>([]);
onMounted(() => { onMounted(() => {
@ -57,7 +57,7 @@ async function fetchGraphData() {
try { try {
let response: Response; let response: Response;
do { do {
response = await GET(`${repoLink.value}/activity/recent-commits/data`); response = await GET(`${repoLink}/activity/recent-commits/data`);
if (response.status === 202) { if (response.status === 202) {
await sleep(1000); // wait for 1 second before retrying await sleep(1000); // wait for 1 second before retrying
} }

View File

@ -1,9 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import ViewFileTreeItem from './ViewFileTreeItem.vue'; import ViewFileTreeItem from './ViewFileTreeItem.vue';
import {onMounted, ref} from 'vue'; import {onMounted, useTemplateRef} from 'vue';
import {createViewFileTreeStore} from './ViewFileTreeStore.ts'; import {createViewFileTreeStore} from './ViewFileTreeStore.ts';
const elRoot = ref<HTMLElement | null>(null); const elRoot = useTemplateRef('elRoot');
const props = defineProps({ const props = defineProps({
repoLink: {type: String, required: true}, repoLink: {type: String, required: true},

View File

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import {SvgIcon} from '../svg.ts'; import {SvgIcon} from '../svg.ts';
import {isPlainClick} from '../utils/dom.ts'; import {isPlainClick} from '../utils/dom.ts';
import {ref} from 'vue'; import {shallowRef} from 'vue';
import {type createViewFileTreeStore} from './ViewFileTreeStore.ts'; import {type createViewFileTreeStore} from './ViewFileTreeStore.ts';
type Item = { type Item = {
@ -20,9 +20,9 @@ const props = defineProps<{
}>(); }>();
const store = props.store; const store = props.store;
const isLoading = ref(false); const isLoading = shallowRef(false);
const children = ref(props.item.children); const children = shallowRef(props.item.children);
const collapsed = ref(!props.item.children); const collapsed = shallowRef(!props.item.children);
const doLoadChildren = async () => { const doLoadChildren = async () => {
collapsed.value = !collapsed.value; collapsed.value = !collapsed.value;

View File

@ -5,7 +5,7 @@ import {confirmModal} from './comp/ConfirmModal.ts';
import type {RequestOpts} from '../types.ts'; import type {RequestOpts} from '../types.ts';
import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts'; import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
const {appSubUrl, i18n} = window.config; const {appSubUrl} = window.config;
// fetchActionDoRedirect does real redirection to bypass the browser's limitations of "location" // fetchActionDoRedirect does real redirection to bypass the browser's limitations of "location"
// more details are in the backend's fetch-redirect handler // more details are in the backend's fetch-redirect handler
@ -23,11 +23,20 @@ function fetchActionDoRedirect(redirect: string) {
} }
async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: RequestOpts) { async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: RequestOpts) {
const showErrorForResponse = (code: number, message: string) => {
showErrorToast(`Error ${code || 'request'}: ${message}`);
};
let respStatus = 0;
let respText = '';
try { try {
hideToastsAll(); hideToastsAll();
const resp = await request(url, opt); const resp = await request(url, opt);
if (resp.status === 200) { respStatus = resp.status;
let {redirect} = await resp.json(); respText = await resp.text();
const respJson = JSON.parse(respText);
if (respStatus === 200) {
let {redirect} = respJson;
redirect = redirect || actionElem.getAttribute('data-redirect'); redirect = redirect || actionElem.getAttribute('data-redirect');
ignoreAreYouSure(actionElem); // ignore the areYouSure check before reloading ignoreAreYouSure(actionElem); // ignore the areYouSure check before reloading
if (redirect) { if (redirect) {
@ -38,22 +47,19 @@ async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: R
return; return;
} }
if (resp.status >= 400 && resp.status < 500) { if (respStatus >= 400 && respStatus < 500 && respJson?.errorMessage) {
const data = await resp.json();
// the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error" // the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error"
// but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond. // but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond.
if (data.errorMessage) { showErrorToast(respJson.errorMessage, {useHtmlBody: respJson.renderFormat === 'html'});
showErrorToast(data.errorMessage, {useHtmlBody: data.renderFormat === 'html'});
} else {
showErrorToast(`server error: ${resp.status}`);
}
} else { } else {
showErrorToast(`server error: ${resp.status}`); showErrorForResponse(respStatus, respText);
} }
} catch (e) { } catch (e) {
if (e.name !== 'AbortError') { if (e.name === 'SyntaxError') {
console.error('error when doRequest', e); showErrorForResponse(respStatus, (respText || '').substring(0, 100));
showErrorToast(`${i18n.network_error} ${e}`); } else if (e.name !== 'AbortError') {
console.error('fetchActionDoRequest error', e);
showErrorForResponse(respStatus, `${e}`);
} }
} }
actionElem.classList.remove('is-loading', 'loading-icon-2px'); actionElem.classList.remove('is-loading', 'loading-icon-2px');