mirror of https://github.com/go-gitea/gitea.git
Compare commits
21 Commits
68ed535bf9
...
0f0b5ef7be
| Author | SHA1 | Date |
|---|---|---|
|
|
0f0b5ef7be | |
|
|
29b28002aa | |
|
|
618e2d8106 | |
|
|
485d8f1121 | |
|
|
181db69e0c | |
|
|
a46b16f10f | |
|
|
52957f4406 | |
|
|
4890a15467 | |
|
|
f5c6a31fc2 | |
|
|
789b73bd55 | |
|
|
f601501fc1 | |
|
|
9234ef5ecc | |
|
|
ae9c0a6e62 | |
|
|
d9efbf30ca | |
|
|
d55e25ff4d | |
|
|
401d2572f9 | |
|
|
52bc49dc8c | |
|
|
10463ec271 | |
|
|
d681313609 | |
|
|
f68e44daed | |
|
|
8b86c3140a |
|
|
@ -653,7 +653,7 @@ func (repo *Repository) AllowsPulls(ctx context.Context) bool {
|
|||
|
||||
// CanEnableEditor returns true if repository meets the requirements of web editor.
|
||||
func (repo *Repository) CanEnableEditor() bool {
|
||||
return !repo.IsMirror
|
||||
return !repo.IsMirror && !repo.IsArchived
|
||||
}
|
||||
|
||||
// DescriptionHTML does special handles to description and return HTML string.
|
||||
|
|
|
|||
|
|
@ -6,8 +6,12 @@ package user
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// Badge represents a user badge
|
||||
|
|
@ -25,6 +29,50 @@ type UserBadge struct { //nolint:revive
|
|||
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() {
|
||||
db.RegisterModel(new(Badge))
|
||||
db.RegisterModel(new(UserBadge))
|
||||
|
|
@ -42,13 +90,37 @@ func GetUserBadges(ctx context.Context, u *User) ([]*Badge, int64, error) {
|
|||
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.
|
||||
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)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetBadge returns a badge
|
||||
// GetBadge returns a specific badge
|
||||
func GetBadge(ctx context.Context, slug string) (*Badge, error) {
|
||||
badge := new(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.
|
||||
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
|
||||
}
|
||||
|
||||
// DeleteBadge deletes a badge.
|
||||
// DeleteBadge deletes a badge and all associated user_badge entries.
|
||||
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 {
|
||||
// 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.
|
||||
|
|
@ -84,7 +168,7 @@ func AddUserBadges(ctx context.Context, u *User, badges []*Badge) error {
|
|||
if err != nil {
|
||||
return err
|
||||
} 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{
|
||||
BadgeID: badge.ID,
|
||||
|
|
@ -102,16 +186,24 @@ func RemoveUserBadge(ctx context.Context, u *User, badge *Badge) error {
|
|||
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 {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
for _, badge := range badges {
|
||||
if _, err := db.GetEngine(ctx).
|
||||
Join("INNER", "badge", "badge.id = `user_badge`.badge_id").
|
||||
Where("`user_badge`.user_id=? AND `badge`.slug=?", u.ID, badge.Slug).
|
||||
Delete(&UserBadge{}); err != nil {
|
||||
slugs := make([]string, len(badges))
|
||||
for i, badge := range badges {
|
||||
slugs[i] = badge.Slug
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
|
|
@ -122,3 +214,56 @@ func RemoveAllUserBadges(ctx context.Context, u *User) error {
|
|||
_, err := db.GetEngine(ctx).Where("user_id=?", u.ID).Delete(&UserBadge{})
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -27,6 +27,8 @@ const (
|
|||
ErrUsername = "UsernameError"
|
||||
// ErrInvalidGroupTeamMap is returned when a group team mapping is invalid
|
||||
ErrInvalidGroupTeamMap = "InvalidGroupTeamMap"
|
||||
// ErrInvalidSlug is returned when a slug is invalid
|
||||
ErrInvalidSlug = "InvalidSlug"
|
||||
)
|
||||
|
||||
// AddBindingRules adds additional binding rules
|
||||
|
|
@ -40,6 +42,7 @@ func AddBindingRules() {
|
|||
addGlobOrRegexPatternRule()
|
||||
addUsernamePatternRule()
|
||||
addValidGroupTeamMapRule()
|
||||
addSlugPatternRule()
|
||||
}
|
||||
|
||||
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() {
|
||||
binding.AddRule(&binding.Rule{
|
||||
IsMatch: func(rule string) bool {
|
||||
|
|
|
|||
|
|
@ -132,3 +132,7 @@ func IsValidUsername(name string) bool {
|
|||
vars := globalVars()
|
||||
return vars.validUsernamePattern.MatchString(name) && !vars.invalidUsernamePattern.MatchString(name)
|
||||
}
|
||||
|
||||
func IsValidSlug(slug string) bool {
|
||||
return IsValidUsername(slug)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
case validation.ErrInvalidGroupTeamMap:
|
||||
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:
|
||||
msg := errs[0].Classification
|
||||
if msg != "" && errs[0].Message != "" {
|
||||
|
|
|
|||
|
|
@ -1368,6 +1368,7 @@ editor.require_signed_commit=Větev vyžaduje podepsaný commit
|
|||
editor.cherry_pick=Cherry-pick %s na:
|
||||
editor.revert=Vrátit %s na:
|
||||
|
||||
|
||||
commits.desc=Procházet historii změn zdrojového kódu.
|
||||
commits.commits=Commity
|
||||
commits.no_commits=Žádné společné commity. „%s“ a „%s“ mají zcela odlišnou historii.
|
||||
|
|
|
|||
|
|
@ -1392,6 +1392,7 @@ editor.require_signed_commit=Branch erfordert einen signierten Commit
|
|||
editor.cherry_pick=Cherry-Picke %s von:
|
||||
editor.revert=%s zurücksetzen auf:
|
||||
|
||||
|
||||
commits.desc=Durchsuche die Quellcode-Änderungshistorie.
|
||||
commits.commits=Commits
|
||||
commits.no_commits=Keine gemeinsamen Commits. "%s" und "%s" haben vollständig unterschiedliche Historien.
|
||||
|
|
|
|||
|
|
@ -1226,6 +1226,7 @@ editor.require_signed_commit=Ο κλάδος απαιτεί υπογεγραμμ
|
|||
editor.cherry_pick=Ανθολόγηση (cherry-pic) του %s στο:
|
||||
editor.revert=Απόσυρση του %s στο:
|
||||
|
||||
|
||||
commits.desc=Δείτε το ιστορικό αλλαγών του πηγαίου κώδικα.
|
||||
commits.commits=Υποβολές
|
||||
commits.no_commits=Δεν υπάρχουν κοινές υποβολές. Τα "%s" και "%s" έχουν εντελώς διαφορετικές ιστορίες.
|
||||
|
|
|
|||
|
|
@ -180,6 +180,7 @@ exact = Exact
|
|||
exact_tooltip = Include only results that match the exact search term
|
||||
repo_kind = Search repos...
|
||||
user_kind = Search users...
|
||||
badge_kind = Search badges...
|
||||
org_kind = Search orgs...
|
||||
team_kind = Search teams...
|
||||
code_kind = Search code...
|
||||
|
|
@ -574,6 +575,7 @@ PayloadUrl = Payload URL
|
|||
TeamName = Team name
|
||||
AuthName = Authorization name
|
||||
AdminEmail = Admin email
|
||||
ImageURL = Image URL
|
||||
|
||||
NewBranchName = New branch name
|
||||
CommitSummary = Commit summary
|
||||
|
|
@ -603,12 +605,15 @@ unknown_error = Unknown error:
|
|||
captcha_incorrect = The CAPTCHA code is incorrect.
|
||||
password_not_match = The passwords do not match.
|
||||
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_change_not_local_user = Non-local users are not allowed to change their username.
|
||||
change_username_disabled = Changing username is disabled.
|
||||
change_full_name_disabled = Changing full name is disabled.
|
||||
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.
|
||||
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.
|
||||
|
|
@ -1399,6 +1404,13 @@ editor.revert = Revert %s onto:
|
|||
editor.failed_to_commit = Failed to commit changes.
|
||||
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.commits = Commits
|
||||
commits.no_commits = No commits in common. "%s" and "%s" have entirely different histories.
|
||||
|
|
@ -2946,6 +2958,7 @@ dashboard = Dashboard
|
|||
self_check = Self Check
|
||||
identity_access = Identity & Access
|
||||
users = User Accounts
|
||||
badges = Badges
|
||||
organizations = Organizations
|
||||
assets = Code Assets
|
||||
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.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.name = Name
|
||||
orgs.teams = Teams
|
||||
|
|
@ -3832,6 +3869,7 @@ runs.no_runs = The workflow has no runs yet.
|
|||
runs.empty_commit_message = (empty commit message)
|
||||
runs.expire_log_message = Logs have been purged because they were too old.
|
||||
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.not_done = This workflow run is not done.
|
||||
runs.view_workflow_file = View workflow file
|
||||
|
|
|
|||
|
|
@ -1216,6 +1216,7 @@ editor.require_signed_commit=Esta rama requiere un commit firmado
|
|||
editor.cherry_pick=Hacer Cherry-pick %s en:
|
||||
editor.revert=Revertir %s en:
|
||||
|
||||
|
||||
commits.desc=Ver el historial de cambios de código fuente.
|
||||
commits.commits=Commits
|
||||
commits.no_commits=No hay commits en común. "%s" y "%s" tienen historias totalmente diferentes.
|
||||
|
|
|
|||
|
|
@ -949,6 +949,7 @@ editor.no_commit_to_branch=نمیتوان به طور مستقیم درمور
|
|||
editor.user_no_push_to_branch=کاربر نمیتواند به شاخه ارسال کند
|
||||
editor.require_signed_commit=شاخه یک کامیت امضا شده لازم دارد
|
||||
|
||||
|
||||
commits.desc=تاریخچه تغییرات کد منبع را مرور کنید.
|
||||
commits.commits=کامیتها
|
||||
commits.nothing_to_compare=این شاخه ها برابرند.
|
||||
|
|
|
|||
|
|
@ -764,6 +764,7 @@ editor.no_changes_to_show=Ei muutoksia näytettäväksi.
|
|||
editor.add_subdir=Lisää hakemisto…
|
||||
editor.require_signed_commit=Haara vaatii vahvistetun commitin
|
||||
|
||||
|
||||
commits.commits=Commitit
|
||||
commits.nothing_to_compare=Nämä haarat vastaavat toisiaan.
|
||||
commits.search_all=Kaikki haarat
|
||||
|
|
|
|||
|
|
@ -1394,6 +1394,7 @@ editor.require_signed_commit=Cette branche nécessite une révision signée
|
|||
editor.cherry_pick=Picorer %s vers:
|
||||
editor.revert=Rétablir %s sur:
|
||||
|
||||
|
||||
commits.desc=Naviguer dans l'historique des modifications.
|
||||
commits.commits=Révisions
|
||||
commits.no_commits=Pas de révisions en commun. "%s" et "%s" ont des historiques entièrement différents.
|
||||
|
|
|
|||
|
|
@ -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=Dearmad ar an bPasfhocal?
|
||||
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 d’ainm ú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_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ú.
|
||||
|
|
@ -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.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.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_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.
|
||||
|
|
@ -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.cherry_pick=Roghnaigh silíní %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.commits=Tiomáintí
|
||||
|
|
@ -2807,6 +2812,7 @@ team_permission_desc=Cead
|
|||
team_unit_desc=Ceadaigh Rochtain ar Rannóga Stóras
|
||||
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_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ú.
|
||||
|
|
@ -2829,12 +2835,27 @@ settings.visibility.private_shortname=Príobháideach
|
|||
settings.update_settings=Nuashonrú Socruithe
|
||||
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 d’eagraí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.delete=Scrios Eagraíocht
|
||||
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.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.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.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.
|
||||
|
|
|
|||
|
|
@ -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.add_subdir=Mappa hozzáadása…
|
||||
|
||||
|
||||
commits.commits=Commit-ok
|
||||
commits.search_all=Minden ág
|
||||
commits.author=Szerző
|
||||
|
|
|
|||
|
|
@ -717,6 +717,7 @@ editor.new_branch_name_desc=Nama branch baru…
|
|||
editor.cancel=Membatalkan
|
||||
editor.no_changes_to_show=Tidak ada perubahan untuk ditampilkan.
|
||||
|
||||
|
||||
commits.commits=Melakukan
|
||||
commits.author=Penulis
|
||||
commits.message=Pesan
|
||||
|
|
|
|||
|
|
@ -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.cancel=Hætta við
|
||||
|
||||
|
||||
commits.commits=Framlög
|
||||
commits.author=Höfundur
|
||||
commits.message=Skilaboð
|
||||
|
|
|
|||
|
|
@ -1024,6 +1024,7 @@ editor.require_signed_commit=Il branch richiede un commit firmato
|
|||
editor.cherry_pick=Cherry-pick %s suto:
|
||||
editor.revert=Ripristina %s su:
|
||||
|
||||
|
||||
commits.desc=Sfoglia la cronologia di modifiche del codice rogente.
|
||||
commits.commits=Commit
|
||||
commits.nothing_to_compare=Questi rami sono uguali.
|
||||
|
|
|
|||
|
|
@ -421,6 +421,7 @@ remember_me.compromised=ログイントークンはもう有効ではなく、
|
|||
forgot_password_title=パスワードを忘れた
|
||||
forgot_password=パスワードをお忘れですか?
|
||||
need_account=アカウントが必要ですか?
|
||||
sign_up_tip=管理者権限を持つ、このシステムの最初のアカウントを登録しようとしています。 ユーザー名とパスワードをよく覚えておいてください。 ユーザー名またはパスワードを忘れた場合は、Giteaのドキュメントを参照してアカウントを復元してください。
|
||||
sign_up_now=登録はこちら。
|
||||
sign_up_successful=アカウントは無事に作成されました。ようこそ!
|
||||
confirmation_mail_sent_prompt_ex=新しい確認メールを <b>%s</b> に送信しました。 %s以内にメールボックスを確認し、登録手続きを完了してください。 登録メールアドレスが間違っている場合は、もういちどサインインすると変更することができます。
|
||||
|
|
@ -1332,7 +1333,9 @@ editor.upload_file=ファイルをアップロード
|
|||
editor.edit_file=ファイルを編集
|
||||
editor.preview_changes=変更をプレビュー
|
||||
editor.cannot_edit_lfs_files=LFSのファイルはWebインターフェースで編集できません。
|
||||
editor.cannot_edit_too_large_file=このファイルは大きすぎるため、編集できません。
|
||||
editor.cannot_edit_non_text_files=バイナリファイルはWebインターフェースで編集できません。
|
||||
editor.file_not_editable_hint=名前の変更や移動は可能です。
|
||||
editor.edit_this_file=ファイルを編集
|
||||
editor.this_file_locked=ファイルはロックされています
|
||||
editor.must_be_on_a_branch=このファイルを変更したり変更の提案をするには、ブランチ上にいる必要があります。
|
||||
|
|
@ -1352,6 +1355,7 @@ editor.update=%s を更新
|
|||
editor.delete=%s を削除
|
||||
editor.patch=パッチの適用
|
||||
editor.patching=パッチ:
|
||||
editor.fail_to_apply_patch=パッチを適用できません
|
||||
editor.new_patch=新しいパッチ
|
||||
editor.commit_message_desc=詳細な説明を追加…
|
||||
editor.signoff_desc=コミットログメッセージの最後にコミッターの Signed-off-by 行を追加
|
||||
|
|
@ -1371,6 +1375,7 @@ editor.branch_already_exists=ブランチ "%s" は、このリポジトリに既
|
|||
editor.directory_is_a_file=ディレクトリ名 "%s" はすでにリポジトリ内のファイルで使用されています。
|
||||
editor.file_is_a_symlink=`"%s" はシンボリックリンクです。 シンボリックリンクはWebエディターで編集できません。`
|
||||
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_already_exists=ファイル "%s" は、このリポジトリに既に存在します。
|
||||
editor.commit_id_not_matching=コミットIDが編集を開始したときのIDと一致しません。 パッチ用のブランチにコミットしたあとマージしてください。
|
||||
|
|
@ -1391,6 +1396,9 @@ editor.user_no_push_to_branch=ユーザーはブランチにプッシュでき
|
|||
editor.require_signed_commit=ブランチでは署名されたコミットが必須です
|
||||
editor.cherry_pick=チェリーピック %s:
|
||||
editor.revert=リバート %s:
|
||||
editor.failed_to_commit=変更のコミットに失敗しました。
|
||||
editor.failed_to_commit_summary=エラーメッセージ:
|
||||
|
||||
|
||||
commits.desc=ソースコードの変更履歴を参照します。
|
||||
commits.commits=コミット
|
||||
|
|
@ -1555,6 +1563,7 @@ issues.filter_user_placeholder=ユーザーを検索
|
|||
issues.filter_user_no_select=すべてのユーザー
|
||||
issues.filter_type=タイプ
|
||||
issues.filter_type.all_issues=すべてのイシュー
|
||||
issues.filter_type.all_pull_requests=すべてのプルリクエスト
|
||||
issues.filter_type.assigned_to_you=自分が担当
|
||||
issues.filter_type.created_by_you=自分が作成
|
||||
issues.filter_type.mentioning_you=自分が関係
|
||||
|
|
@ -1646,6 +1655,7 @@ issues.save=保存
|
|||
issues.label_title=名前
|
||||
issues.label_description=説明
|
||||
issues.label_color=カラー
|
||||
issues.label_color_invalid=無効な色です
|
||||
issues.label_exclusive=排他
|
||||
issues.label_archive=アーカイブ ラベル
|
||||
issues.label_archived_filter=アーカイブされたラベルを表示
|
||||
|
|
@ -2394,6 +2404,8 @@ settings.event_pull_request_review_request_desc=プルリクエストのレビ
|
|||
settings.event_pull_request_approvals=プルリクエストの承認
|
||||
settings.event_pull_request_merge=プルリクエストのマージ
|
||||
settings.event_header_workflow=ワークフローイベント
|
||||
settings.event_workflow_run=ワークフロー実行
|
||||
settings.event_workflow_run_desc=Gitea Actions のワークフロー実行が、キューに追加、待機中、実行中、完了になったとき。
|
||||
settings.event_workflow_job=ワークフロージョブ
|
||||
settings.event_workflow_job_desc=Gitea Actions のワークフロージョブが、キューに追加、待機中、実行中、完了になったとき。
|
||||
settings.event_package=パッケージ
|
||||
|
|
@ -2801,6 +2813,7 @@ team_permission_desc=権限
|
|||
team_unit_desc=リポジトリのセクションへのアクセスを許可
|
||||
team_unit_disabled=(無効)
|
||||
|
||||
form.name_been_taken=組織名 "%s" は既に使用されています。
|
||||
form.name_reserved=組織名 "%s" は予約されています。
|
||||
form.name_pattern_not_allowed=`"%s" の形式は組織名に使用できません。`
|
||||
form.create_org_not_allowed=組織を作成する権限がありません。
|
||||
|
|
@ -2823,12 +2836,27 @@ settings.visibility.private_shortname=プライベート
|
|||
settings.update_settings=設定の更新
|
||||
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.delete=組織を削除
|
||||
settings.delete_account=この組織を削除
|
||||
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.delete_failed=内部エラーのため組織を削除できませんでした
|
||||
settings.delete_successful=組織の<b>%s</b>の削除に成功しました。
|
||||
settings.hooks_desc=この組織の<strong>すべてのリポジトリ</strong>でトリガーされるWebhookを追加します。
|
||||
|
||||
settings.labels_desc=この組織の<strong>すべてのリポジトリ</strong>で使用可能なイシューラベルを追加します。
|
||||
|
|
|
|||
|
|
@ -648,6 +648,7 @@ editor.filename_cannot_be_empty=파일명이 빈칸입니다.
|
|||
editor.no_changes_to_show=표시할 변경사항이 없습니다.
|
||||
editor.add_subdir=경로 추가...
|
||||
|
||||
|
||||
commits.desc=소스 코드 변경 내역 탐색
|
||||
commits.commits=커밋
|
||||
commits.search_all=모든 브랜치
|
||||
|
|
|
|||
|
|
@ -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.revert=Atgriezt %s uz:
|
||||
|
||||
|
||||
commits.desc=Pārlūkot pirmkoda izmaiņu vēsturi.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -1022,6 +1022,7 @@ editor.require_signed_commit=Branch vereist een ondertekende commit
|
|||
editor.cherry_pick=Cherry-pick %s op:
|
||||
editor.revert=%s ongedaan maken op:
|
||||
|
||||
|
||||
commits.desc=Bekijk de broncode-wijzigingsgeschiedenis.
|
||||
commits.commits=Commits
|
||||
commits.nothing_to_compare=Deze branches zijn gelijk.
|
||||
|
|
|
|||
|
|
@ -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.require_signed_commit=Gałąź wymaga podpisanych commitów
|
||||
|
||||
|
||||
commits.desc=Przeglądaj historię zmian kodu źródłowego.
|
||||
commits.commits=Commity
|
||||
commits.search_all=Wszystkie gałęzie
|
||||
|
|
|
|||
|
|
@ -1227,6 +1227,7 @@ editor.require_signed_commit=Branch requer um commit assinado
|
|||
editor.cherry_pick=Cherry-pick %s para:
|
||||
editor.revert=Reverter %s para:
|
||||
|
||||
|
||||
commits.desc=Veja o histórico de alterações do código de fonte.
|
||||
commits.commits=Commits
|
||||
commits.no_commits=Nenhum commit em comum. "%s" e "%s" tem históricos completamente diferentes.
|
||||
|
|
|
|||
|
|
@ -1393,6 +1393,7 @@ editor.require_signed_commit=O ramo requer um cometimento assinado
|
|||
editor.cherry_pick=Escolher a dedo %s para:
|
||||
editor.revert=Reverter %s para:
|
||||
|
||||
|
||||
commits.desc=Navegar pelo histórico de modificações no código fonte.
|
||||
commits.commits=Cometimentos
|
||||
commits.no_commits=Não há cometimentos em comum. "%s" e "%s" têm históricos completamente diferentes.
|
||||
|
|
|
|||
|
|
@ -1205,6 +1205,7 @@ editor.require_signed_commit=Ветка ожидает подписанный к
|
|||
editor.cherry_pick=Перенести изменения %s в:
|
||||
editor.revert=Откатить %s к:
|
||||
|
||||
|
||||
commits.desc=Просмотр истории изменений исходного кода.
|
||||
commits.commits=Коммитов
|
||||
commits.no_commits=Нет общих коммитов. «%s» и «%s» имеют совершенно разные истории.
|
||||
|
|
|
|||
|
|
@ -923,6 +923,7 @@ editor.no_commit_to_branch=ශාඛාවට කෙලින්ම කැපව
|
|||
editor.user_no_push_to_branch=පරිශීලකයාට ශාඛාවට තල්ලු කළ නොහැක
|
||||
editor.require_signed_commit=ශාඛාවට අත්සන් කළ කැපවීමක් අවශ්ය වේ
|
||||
|
||||
|
||||
commits.desc=මූලාශ්ර කේත වෙනස් කිරීමේ ඉතිහාසය පිරික්සන්න.
|
||||
commits.commits=විවරයන්
|
||||
commits.nothing_to_compare=මෙම ශාඛා සමාන වේ.
|
||||
|
|
|
|||
|
|
@ -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.require_signed_commit=Vetva vyžaduje podpísaný commit
|
||||
|
||||
|
||||
commits.commits=Commity
|
||||
commits.search_all=Všetky vetvy
|
||||
commits.author=Autor
|
||||
|
|
|
|||
|
|
@ -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.require_signed_commit=Branchen kräver en signerad commit
|
||||
|
||||
|
||||
commits.desc=Bläddra i källkodens förändringshistorik.
|
||||
commits.commits=Incheckningar
|
||||
commits.search_all=Alla brancher
|
||||
|
|
|
|||
|
|
@ -1385,6 +1385,7 @@ editor.require_signed_commit=Dal imzalı bir işleme gerektirir
|
|||
editor.cherry_pick=%s şunun üzerine cımbızla:
|
||||
editor.revert=%s şuna geri döndür:
|
||||
|
||||
|
||||
commits.desc=Kaynak kodu değişiklik geçmişine göz atın.
|
||||
commits.commits=İşleme
|
||||
commits.no_commits=Ortak bir işleme yok. "%s" ve "%s" tamamen farklı geçmişlere sahip.
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ remove=Видалити
|
|||
remove_all=Видалити все
|
||||
remove_label_str=`Видалити елемент "%s"`
|
||||
edit=Редагувати
|
||||
view=Переглянути
|
||||
test=Тест
|
||||
|
||||
enabled=Увімкнено
|
||||
|
|
@ -414,6 +415,7 @@ remember_me.compromised=Токен для входу більше не дійс
|
|||
forgot_password_title=Забув пароль
|
||||
forgot_password=Забули пароль?
|
||||
need_account=Потрібен обліковий запис?
|
||||
sign_up_tip=Ви реєструєте перший обліковий запис у системі, з правами адміністратора. Будь ласка, уважно запам'ятайте своє ім'я користувача та пароль. Якщо ви їх забудете, зверніться до документації Gitea, щоб відновити обліковий запис.
|
||||
sign_up_now=Зареєструватися.
|
||||
sign_up_successful=Обліковий запис створено успішно. Вітаю!
|
||||
confirmation_mail_sent_prompt_ex=Новий лист з підтвердженням було надіслано на <b>%s</b>. Будь ласка, перевірте свою поштову скриньку протягом наступних %s, щоб завершити процес реєстрації. Якщо ви вказали невірну адресу електронної пошти, ви можете увійти ще раз і змінити її.
|
||||
|
|
@ -970,6 +972,7 @@ passcode_invalid=Некоректний пароль. Спробуй ще раз
|
|||
twofa_failed_get_secret=Не вдалося отримати код.
|
||||
|
||||
webauthn_register_key=Додати ключ безпеки
|
||||
webauthn_nickname=Псевдонім
|
||||
webauthn_delete_key=Видалити ключ безпеки
|
||||
webauthn_delete_key_desc=Якщо ви видалите ключ безпеки, ви більше не зможете ввійти за його допомогою. Продовжити?
|
||||
webauthn_key_loss_warning=Якщо ви втратите ключі безпеки, ви втратите доступ до свого облікового запису.
|
||||
|
|
@ -1336,6 +1339,7 @@ editor.commit_email=Електронна пошта коміту
|
|||
editor.invalid_commit_email=Адреса електронної пошти для коміту недійсна.
|
||||
editor.file_is_a_symlink=`"%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.commit_empty_file_header=Закомітити порожній файл
|
||||
editor.commit_empty_file_text=Файл, який ви збираєтеся закомітити, порожній. Продовжувати?
|
||||
|
|
@ -1349,9 +1353,14 @@ editor.upload_file_is_locked=Файл "%s" заблоковано %s.
|
|||
editor.upload_files_to_dir=`Завантажити файли до "%s"`
|
||||
editor.no_commit_to_branch=Не вдалося внести коміт безпосередньо до гілки, тому що:
|
||||
editor.require_signed_commit=Гілка вимагає підписаного коміту
|
||||
editor.revert=Повернути %s до:
|
||||
editor.failed_to_commit=Не вдалося зафіксувати зміни.
|
||||
editor.failed_to_commit_summary=Помилка:
|
||||
|
||||
|
||||
commits.desc=Переглянути історію зміни коду.
|
||||
commits.commits=Коміти
|
||||
commits.no_commits=Немає спільних комітів. '%s' та '%s' мають різну історію.
|
||||
commits.nothing_to_compare=Ці гілки однакові.
|
||||
commits.search_branch=Ця гілка
|
||||
commits.search_all=Усі гілки
|
||||
|
|
@ -1368,6 +1377,7 @@ commits.ssh_key_fingerprint=Відбиток ключа SSH
|
|||
commits.view_path=Переглянути в історії
|
||||
commits.view_file_diff=Переглянути зміни до цього файлу в цьому коміті
|
||||
|
||||
commit.operations=Дії
|
||||
commit.revert=Повернути до попереднього стану
|
||||
commit.revert-header=Повернути: %s
|
||||
commit.revert-content=Виберіть гілку, до якої хочете повернутися:
|
||||
|
|
@ -1442,6 +1452,7 @@ issues.new.clear_assignees=Прибрати виконавців
|
|||
issues.new.no_assignees=Немає виконавців
|
||||
issues.new.no_reviewers=Немає рецензентів
|
||||
issues.new.blocked_user=Не вдалося створити задачу, тому що ви заблоковані власником сховища.
|
||||
issues.edit.blocked_user=Неможливо редагувати вміст, оскільки вас заблоковано автором або власником сховища.
|
||||
issues.choose.get_started=Розпочати
|
||||
issues.choose.open_external_link=Відкрити
|
||||
issues.choose.blank=Типово
|
||||
|
|
@ -1459,6 +1470,7 @@ issues.label_templates.title=Завантажити визначений наб
|
|||
issues.label_templates.info=Ще немає міток. Натисніть 'Нова мітка' або використовуйте попередньо визначений набір міток:
|
||||
issues.label_templates.helper=Оберіть набір міток
|
||||
issues.label_templates.use=Використати набір міток
|
||||
issues.label_templates.fail_to_load_file=Не вдалося завантажити файл шаблона мітки '%s': %v
|
||||
issues.add_label=додано %s з міткою %s
|
||||
issues.add_labels=додано %s з мітками %s
|
||||
issues.remove_label=видалено %s з міткою %s
|
||||
|
|
@ -1494,6 +1506,7 @@ issues.filter_project=Проєкт
|
|||
issues.filter_project_all=Всі проєкти
|
||||
issues.filter_project_none=Проєкт відсутній
|
||||
issues.filter_assignee=Виконавець
|
||||
issues.filter_assignee_no_assignee=Нікому не присвоєно
|
||||
issues.filter_assignee_any_assignee=Призначено будь-кому
|
||||
issues.filter_poster=Автор
|
||||
issues.filter_user_placeholder=Пошук користувачів
|
||||
|
|
@ -1504,6 +1517,7 @@ issues.filter_type.all_pull_requests=Усі запити на злиття
|
|||
issues.filter_type.assigned_to_you=Призначене вам
|
||||
issues.filter_type.created_by_you=Створено вами
|
||||
issues.filter_type.mentioning_you=Вас згадано
|
||||
issues.filter_type.review_requested=Запит на рецензію
|
||||
issues.filter_type.reviewed_by_you=Перевірено вами
|
||||
issues.filter_sort=Сортувати
|
||||
issues.filter_sort.latest=Найновіші
|
||||
|
|
@ -1543,7 +1557,9 @@ issues.context.quote_reply=Цитувати відповідь
|
|||
issues.context.reference_issue=Посилання в новій задачі
|
||||
issues.context.edit=Редагувати
|
||||
issues.context.delete=Видалити
|
||||
issues.no_content=Немає опису.
|
||||
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.close_comment_issue=Закрити з коментарем
|
||||
issues.reopen_issue=Відкрити знову
|
||||
|
|
@ -1586,6 +1602,7 @@ issues.label_title=Назва мітки
|
|||
issues.label_description=Опис мітки
|
||||
issues.label_color=Колір
|
||||
issues.label_color_invalid=Недійсний колір
|
||||
issues.label_exclusive=Ексклюзивно
|
||||
issues.label_archive=Мітка архіву
|
||||
issues.label_archived_filter=Показати архівовані мітки
|
||||
issues.label_archive_tooltip=Архівовані мітки типово виключаються з пропозицій під час пошуку за мітками.
|
||||
|
|
@ -1777,7 +1794,13 @@ pulls.switch_comparison_type=Перемкнути тип порівняння
|
|||
pulls.switch_head_and_base=Поміняти місцями основну та базову гілку
|
||||
pulls.filter_branch=Фільтр по гілці
|
||||
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_have_tag=Виділена гілка або мітка ідентичні.
|
||||
pulls.nothing_to_compare_and_allow_empty_pr=Одинакові гілки. Цей PR буде порожнім.
|
||||
pulls.has_pull_request=`Запит злиття для цих гілок вже існує: <a href="%[1]s">%[2]s#%[3]d</a>`
|
||||
pulls.create=Створити запит на злиття
|
||||
|
|
@ -1790,6 +1813,7 @@ pulls.tab_files=Змінені файли
|
|||
pulls.reopen_to_merge=Будь ласка, заново відкрийте цей запит щоб виконати злиття.
|
||||
pulls.cant_reopen_deleted_branch=Цей запит не можна повторно відкрити, оскільки гілку видалено.
|
||||
pulls.merged=Злито
|
||||
pulls.merged_success=Запит на злиття успішно об'єднано і закрито
|
||||
pulls.closed=Запит на злиття закрито
|
||||
pulls.manually_merged=Ручне злиття
|
||||
pulls.merged_info_text=Гілку %s тепер можна видалити.
|
||||
|
|
@ -1802,6 +1826,7 @@ pulls.remove_prefix=Видалити префікс <strong>%s</strong>
|
|||
pulls.data_broken=Збій цього запиту на злиття через відсутність інформації про форк.
|
||||
pulls.files_conflicted=Цей запит на злиття має зміни, що конфліктують з цільовою гілкою.
|
||||
pulls.is_checking=Перевірка конфліктів об'єднання (merge) ...
|
||||
pulls.is_ancestor=Цю гілку вже включено до цільової гілки. Нема чого об'єднувати.
|
||||
pulls.required_status_check_failed=Деякі необхідні перевірки виконані з помилками.
|
||||
pulls.required_status_check_missing=Декілька з необхідних перевірок відсутні.
|
||||
pulls.required_status_check_administrator=Як адміністратор ви все одно можете об'єднати цей запит на злиття.
|
||||
|
|
@ -1869,6 +1894,9 @@ pulls.auto_merge_not_scheduled=Цей запит на злиття не план
|
|||
|
||||
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"?
|
||||
|
||||
pull.deleted_branch=(видалена):%s
|
||||
|
|
@ -2248,6 +2276,10 @@ settings.event_pull_request_review=Запит на злиття рецензов
|
|||
settings.event_pull_request_review_desc=Запит на злиття підтверджено, відхилено або прокоментовано.
|
||||
settings.event_pull_request_sync=Запит на злиття синхронізується
|
||||
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.branch_filter=Фільтр гілок
|
||||
settings.authorization_header=Заголовок авторизації
|
||||
|
|
@ -2586,6 +2618,7 @@ team_permission_desc=Права доступу
|
|||
team_unit_desc=Дозволити доступ до розділів репозиторію
|
||||
team_unit_disabled=(Вимкнено)
|
||||
|
||||
form.name_been_taken=Назва організації "%s" вже зайнята.
|
||||
form.name_reserved=Назву організації "%s" зарезервовано.
|
||||
form.name_pattern_not_allowed=Шаблон "%s" не допускається в назві організації.
|
||||
form.create_org_not_allowed=Вам не дозволено створювати організації.
|
||||
|
|
@ -2608,12 +2641,27 @@ settings.visibility.private_shortname=Приватний
|
|||
settings.update_settings=Оновити налаштування
|
||||
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.delete=Видалити організацію
|
||||
settings.delete_account=Видалити цю організацію
|
||||
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.delete_failed=Не вдалося видалити організацію через внутрішню помилку
|
||||
settings.delete_successful=Організацію <b>%s</b> успішно видалено.
|
||||
settings.hooks_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_helper=Учасники можуть створювати нові репозиторії в організації. Автор отримає доступ адміністратора до нового репозиторію.
|
||||
teams.none_access=Немає доступу
|
||||
teams.none_access_helper=Учасники не можуть переглядати або виконувати будь-які інші дії з цією одиницею. Це не впливає на загальнодоступні сховища.
|
||||
teams.general_access=Загальний доступ
|
||||
teams.general_access_helper=Дозволи учасників будуть визначатися відповідно до наведеної нижче таблиці дозволів.
|
||||
teams.read_access=Читання
|
||||
|
|
@ -2667,6 +2716,7 @@ teams.remove_all_repos_title=Видалити всі репозиторії ко
|
|||
teams.remove_all_repos_desc=Це видалить усі репозиторії команди.
|
||||
teams.add_all_repos_title=Додати всі репозиторії
|
||||
teams.add_all_repos_desc=Це додасть всі репозиторії організації до команди.
|
||||
teams.add_nonexistent_repo=Сховище, яке ви намагаєтеся додати, не існує, будь ласка, створіть його спочатку.
|
||||
teams.add_duplicate_users=Користувач уже є членом команди.
|
||||
teams.repos.none=Для команди немає доступних репозиторіїв.
|
||||
teams.members.none=Немає членів в цій команді.
|
||||
|
|
@ -2758,6 +2808,7 @@ dashboard.resync_all_hooks=Заново синхронізувати хуки п
|
|||
dashboard.reinit_missing_repos=Заново ініціалізувати всі відсутні сховища Git'а, для яких існують записи
|
||||
dashboard.sync_external_users=Синхронізувати дані зовнішніх користувачів
|
||||
dashboard.cleanup_hook_task_table=Очистити таблицю hook_task
|
||||
dashboard.cleanup_packages=Очистити застарілі пакети
|
||||
dashboard.cleanup_actions=Очищення ресурсів прострочених дій
|
||||
dashboard.server_uptime=Час роботи сервера
|
||||
dashboard.current_goroutine=Поточна кількість Goroutines
|
||||
|
|
@ -2789,11 +2840,15 @@ dashboard.total_gc_pause=Загальна пауза збирача сміття
|
|||
dashboard.last_gc_pause=Остання пауза збирача сміття (GC)
|
||||
dashboard.gc_times=Кількість запусків збирача сміття (GC)
|
||||
dashboard.delete_old_actions=Видалити всі старі дії з бази даних
|
||||
dashboard.delete_old_actions.started=Видалення всіх старих дій з бази даних розпочато.
|
||||
dashboard.update_checker=Перевірка оновлень
|
||||
dashboard.delete_old_system_notices=Видалити всі старі системні повідомлення з бази даних
|
||||
dashboard.gc_lfs=Збір сміття мета-об'єктів LFS
|
||||
dashboard.stop_endless_tasks=Зупинити нескінченні завдання
|
||||
dashboard.cancel_abandoned_jobs=Скасувати покинуті завдання
|
||||
dashboard.start_schedule_tasks=Запуск запланованих завдань
|
||||
dashboard.sync_branch.started=Розпочато синхронізацію гілок
|
||||
dashboard.sync_tag.started=Розпочато синхронізацію міток
|
||||
dashboard.rebuild_issue_indexer=Перебудувати індексатор задач
|
||||
dashboard.sync_repo_licenses=Синхронізувати ліцензії сховища
|
||||
|
||||
|
|
@ -3187,6 +3242,7 @@ monitor.queue.settings.remove_all_items_done=Усі елементи черги
|
|||
|
||||
notices.system_notice_list=Сповіщення системи
|
||||
notices.view_detail_header=Переглянути деталі повідомлення
|
||||
notices.operations=Дії
|
||||
notices.select_all=Вибрати все
|
||||
notices.deselect_all=Скасувати виділення
|
||||
notices.inverse_selection=Інвертувати виділене
|
||||
|
|
@ -3254,6 +3310,7 @@ seconds=%d секунди
|
|||
minutes=%d хвилини
|
||||
hours=%d години
|
||||
days=%d дні
|
||||
weeks=%d тижні(в)
|
||||
months=%d місяці
|
||||
years=%d роки
|
||||
raw_seconds=секунди
|
||||
|
|
@ -3324,6 +3381,7 @@ details.license=Ліцензія
|
|||
assets=Ресурси
|
||||
versions=Версії
|
||||
versions.view_all=Переглянути все
|
||||
dependency.id=ID
|
||||
dependency.version=Версія
|
||||
search_in_external_registry=Шукати в %s
|
||||
alpine.registry=Налаштуйте цей реєстр, додавши URL у ваш файл <code>/etc/apk/repositories</code>:
|
||||
|
|
@ -3463,6 +3521,8 @@ actions=Дії
|
|||
unit.desc=Керувати діями
|
||||
|
||||
status.unknown=Невідомий
|
||||
status.waiting=Очікування
|
||||
status.running=Виконується
|
||||
status.success=Успіх
|
||||
status.failure=Невдача
|
||||
status.cancelled=Скасовано
|
||||
|
|
@ -3470,6 +3530,7 @@ status.skipped=Пропущено
|
|||
status.blocked=Заблоковано
|
||||
|
||||
runners.status=Статус
|
||||
runners.id=ID
|
||||
runners.name=Назва
|
||||
runners.owner_type=Тип
|
||||
runners.description=Опис
|
||||
|
|
@ -3492,23 +3553,36 @@ runs.all_workflows=Всі робочі процеси
|
|||
runs.commit=Коміт
|
||||
runs.scheduled=Заплановано
|
||||
runs.pushed_by=завантажено
|
||||
runs.invalid_workflow_helper=Файл конфігурації робочих процесів недійсний. Будь ласка, перевірте файл конфігурації: %s
|
||||
runs.no_job_without_needs=Робочий процес повинен містити принаймні одну задачу без залежностей.
|
||||
runs.no_job=Робочий процес повинен містити принаймні одну задачу
|
||||
runs.actor=Актор
|
||||
runs.status=Статус
|
||||
runs.actors_no_select=Усі актори
|
||||
runs.status_no_select=Всі статуси
|
||||
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_runs=Робочий процес ще не виконувався.
|
||||
runs.empty_commit_message=(порожнє повідомлення коміту)
|
||||
runs.expire_log_message=Журнали були очищені, тому що вони були занадто старі.
|
||||
runs.delete=Видалити запущений робочий процес
|
||||
runs.delete.description=Ви впевнені, що хочете остаточно видалити цей робочий процес? Цю дію неможливо скасувати.
|
||||
runs.not_done=Виконання цього робочого процесу не завершено.
|
||||
runs.view_workflow_file=Перегляд файлу робочого процесу
|
||||
|
||||
workflow.disable=Вимкнути робочий процес
|
||||
workflow.disable_success=Робочий процес '%s' успішно вимкнено.
|
||||
workflow.enable=Увімкнути робочий процес
|
||||
workflow.enable_success=Робочий процес '%s' успішно ввімкнено.
|
||||
workflow.disabled=Робочий процес вимкнений.
|
||||
workflow.run=Запустити робочий процес
|
||||
workflow.not_found=Робочий процес '%s' не знайдено.
|
||||
workflow.run_success=Робочий процес '%s' завершився успішно.
|
||||
workflow.from_ref=Використати робочий процес з
|
||||
workflow.has_workflow_dispatch=Цей робочий процес має тригер події workflow_dispatch.
|
||||
workflow.has_no_workflow_dispatch=Робочий процес “%s” не має тригера події workflow_dispatch.
|
||||
|
||||
|
||||
variables=Змінні
|
||||
|
|
|
|||
|
|
@ -1394,6 +1394,7 @@ editor.require_signed_commit=分支需要签名提交
|
|||
editor.cherry_pick=拣选提交 %s 到:
|
||||
editor.revert=将 %s 还原到:
|
||||
|
||||
|
||||
commits.desc=浏览代码修改历史
|
||||
commits.commits=次代码提交
|
||||
commits.no_commits=没有共同的提交。「%s」和「%s」的历史完全不同。
|
||||
|
|
|
|||
|
|
@ -374,6 +374,7 @@ editor.create_new_branch=建立 <strong>新的分支</strong> 為此提交和開
|
|||
editor.cancel=取消
|
||||
editor.no_changes_to_show=沒有可以顯示的變更。
|
||||
|
||||
|
||||
commits.commits=次程式碼提交
|
||||
commits.author=作者
|
||||
commits.message=備註
|
||||
|
|
|
|||
|
|
@ -1356,6 +1356,7 @@ editor.require_signed_commit=分支僅接受經簽署的提交
|
|||
editor.cherry_pick=Cherry-pick %s 到:
|
||||
editor.revert=還原 %s 到:
|
||||
|
||||
|
||||
commits.desc=瀏覽原始碼修改歷程。
|
||||
commits.commits=次程式碼提交
|
||||
commits.no_commits=沒有共同的提交。「%s」和「%s」的歷史完全不同。
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -318,7 +318,7 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) {
|
|||
ctx.Data["Page"] = pager
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/charset"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
|
|
@ -39,7 +40,7 @@ const (
|
|||
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)
|
||||
if cleanedTreePath != ctx.Repo.TreePath {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
ctx.ServerError("PrepareCommitFormBehaviors", err)
|
||||
return
|
||||
ctx.ServerError("PrepareCommitFormOptions", err)
|
||||
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["TreePath"] = ctx.Repo.TreePath
|
||||
ctx.Data["CommitFormBehaviors"] = commitFormBehaviors
|
||||
ctx.Data["CommitFormOptions"] = commitFormOptions
|
||||
|
||||
// for online editor
|
||||
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
|
||||
|
|
@ -69,25 +80,27 @@ func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) {
|
|||
// form fields
|
||||
ctx.Data["commit_summary"] = ""
|
||||
ctx.Data["commit_message"] = ""
|
||||
ctx.Data["commit_choice"] = util.Iif(commitFormBehaviors.CanCommitToBranch, editorCommitChoiceDirect, editorCommitChoiceNewBranch)
|
||||
ctx.Data["new_branch_name"] = getUniquePatchBranchName(ctx, ctx.Doer.LowerName, ctx.Repo.Repository)
|
||||
ctx.Data["commit_choice"] = util.Iif(commitFormOptions.CanCommitToBranch, editorCommitChoiceDirect, editorCommitChoiceNewBranch)
|
||||
ctx.Data["new_branch_name"] = getUniquePatchBranchName(ctx, ctx.Doer.LowerName, commitFormOptions.TargetRepo)
|
||||
ctx.Data["last_commit"] = ctx.Repo.CommitID
|
||||
return commitFormOptions
|
||||
}
|
||||
|
||||
func prepareTreePathFieldsAndPaths(ctx *context.Context, treePath string) {
|
||||
// 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
|
||||
commonForm *forms.CommitCommonForm
|
||||
CommitFormBehaviors *context.CommitFormBehaviors
|
||||
TargetBranchName string
|
||||
CommitFormOptions *context.CommitFormOptions
|
||||
OldBranchName string
|
||||
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)
|
||||
if body := strings.TrimSpace(f.commonForm.CommitMessage); body != "" {
|
||||
commitMessage += "\n\n" + body
|
||||
|
|
@ -95,7 +108,7 @@ func (f *parsedEditorCommitForm[T]) GetCommitMessage(defaultCommitMessage string
|
|||
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)
|
||||
if ctx.HasError() {
|
||||
ctx.JSONError(ctx.GetErrMsg())
|
||||
|
|
@ -105,15 +118,22 @@ func parseEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *cont
|
|||
commonForm := form.GetCommitCommonForm()
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
// check commit behavior
|
||||
targetBranchName := util.Iif(commonForm.CommitChoice == editorCommitChoiceNewBranch, commonForm.NewBranchName, ctx.Repo.BranchName)
|
||||
if targetBranchName == ctx.Repo.BranchName && !commitFormBehaviors.CanCommitToBranch {
|
||||
fromBaseBranch := ctx.FormString("from_base_branch")
|
||||
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))
|
||||
return nil
|
||||
}
|
||||
|
|
@ -125,28 +145,63 @@ func parseEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *cont
|
|||
return nil
|
||||
}
|
||||
|
||||
return &parsedEditorCommitForm[T]{
|
||||
if commitToNewBranch {
|
||||
// if target branch exists, we should stop
|
||||
targetBranchExists, err := git_model.IsBranchExist(ctx, commitFormOptions.TargetRepo.ID, targetBranchName)
|
||||
if err != nil {
|
||||
ctx.ServerError("IsBranchExist", err)
|
||||
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,
|
||||
CommitFormBehaviors: commitFormBehaviors,
|
||||
TargetBranchName: targetBranchName,
|
||||
CommitFormOptions: commitFormOptions,
|
||||
OldBranchName: oldBranchName,
|
||||
NewBranchName: targetBranchName,
|
||||
GitCommitter: gitCommitter,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Redirect to a pull request when possible
|
||||
redirectToPullRequest := false
|
||||
repo, baseBranch, headBranch := ctx.Repo.Repository, ctx.Repo.BranchName, parsed.TargetBranchName
|
||||
if repo.UnitEnabled(ctx, unit.TypePullRequests) {
|
||||
redirectToPullRequest = true
|
||||
} else if parsed.CommitFormBehaviors.CanCreateBasePullRequest {
|
||||
repo, baseBranch, headBranch := ctx.Repo.Repository, parsed.OldBranchName, parsed.NewBranchName
|
||||
if ctx.Repo.Repository.IsFork && parsed.CommitFormOptions.CanCreateBasePullRequest {
|
||||
redirectToPullRequest = true
|
||||
baseBranch = repo.BaseRepo.DefaultBranch
|
||||
headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch
|
||||
repo = repo.BaseRepo
|
||||
} else if repo.UnitEnabled(ctx, unit.TypePullRequests) {
|
||||
redirectToPullRequest = true
|
||||
}
|
||||
if redirectToPullRequest {
|
||||
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")
|
||||
if returnURI == "" || !httplib.IsCurrentGiteaSiteURL(ctx, returnURI) {
|
||||
returnURI = util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.TargetBranchName), util.PathEscapeSegments(treePath))
|
||||
}
|
||||
ctx.JSONRedirect(returnURI)
|
||||
// redirect to the newly updated file
|
||||
redirectTo := util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.NewBranchName), util.PathEscapeSegments(treePath))
|
||||
ctx.JSONRedirect(redirectTo)
|
||||
}
|
||||
|
||||
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) {
|
||||
editorAction := ctx.PathParam("editor_action")
|
||||
isNewFile := editorAction == "_new"
|
||||
parsed := parseEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
|
||||
parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
|
@ -292,8 +345,8 @@ func EditFilePost(ctx *context.Context) {
|
|||
|
||||
_, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
|
||||
LastCommitID: parsed.form.LastCommit,
|
||||
OldBranch: ctx.Repo.BranchName,
|
||||
NewBranch: parsed.TargetBranchName,
|
||||
OldBranch: parsed.OldBranchName,
|
||||
NewBranch: parsed.NewBranchName,
|
||||
Message: parsed.GetCommitMessage(defaultCommitMessage),
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
|
|
@ -308,7 +361,7 @@ func EditFilePost(ctx *context.Context) {
|
|||
Committer: parsed.GitCommitter,
|
||||
})
|
||||
if err != nil {
|
||||
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
|
||||
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -327,7 +380,7 @@ func DeleteFile(ctx *context.Context) {
|
|||
|
||||
// DeleteFilePost response for deleting file
|
||||
func DeleteFilePost(ctx *context.Context) {
|
||||
parsed := parseEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
|
||||
parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
|
@ -335,8 +388,8 @@ func DeleteFilePost(ctx *context.Context) {
|
|||
treePath := ctx.Repo.TreePath
|
||||
_, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
|
||||
LastCommitID: parsed.form.LastCommit,
|
||||
OldBranch: ctx.Repo.BranchName,
|
||||
NewBranch: parsed.TargetBranchName,
|
||||
OldBranch: parsed.OldBranchName,
|
||||
NewBranch: parsed.NewBranchName,
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "delete",
|
||||
|
|
@ -349,29 +402,29 @@ func DeleteFilePost(ctx *context.Context) {
|
|||
Committer: parsed.GitCommitter,
|
||||
})
|
||||
if err != nil {
|
||||
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
|
||||
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func UploadFile(ctx *context.Context) {
|
||||
ctx.Data["PageIsUpload"] = true
|
||||
upload.AddUploadContext(ctx, "repo")
|
||||
prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath)
|
||||
|
||||
prepareEditorCommitFormOptions(ctx, "_upload")
|
||||
opts := prepareEditorCommitFormOptions(ctx, "_upload")
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
upload.AddUploadContextForRepo(ctx, opts.TargetRepo)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplUploadFile)
|
||||
}
|
||||
|
||||
func UploadFilePost(ctx *context.Context) {
|
||||
parsed := parseEditorCommitSubmittedForm[*forms.UploadRepoFileForm](ctx)
|
||||
parsed := prepareEditorCommitSubmittedForm[*forms.UploadRepoFileForm](ctx)
|
||||
if ctx.Written() {
|
||||
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, "/"))
|
||||
err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{
|
||||
LastCommitID: parsed.form.LastCommit,
|
||||
OldBranch: ctx.Repo.BranchName,
|
||||
NewBranch: parsed.TargetBranchName,
|
||||
OldBranch: parsed.OldBranchName,
|
||||
NewBranch: parsed.NewBranchName,
|
||||
TreePath: parsed.form.TreePath,
|
||||
Message: parsed.GetCommitMessage(defaultCommitMessage),
|
||||
Files: parsed.form.Files,
|
||||
|
|
@ -389,7 +442,7 @@ func UploadFilePost(ctx *context.Context) {
|
|||
Committer: parsed.GitCommitter,
|
||||
})
|
||||
if err != nil {
|
||||
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
|
||||
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
||||
return
|
||||
}
|
||||
redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ func NewDiffPatch(ctx *context.Context) {
|
|||
|
||||
// NewDiffPatchPost response for sending patch page
|
||||
func NewDiffPatchPost(ctx *context.Context) {
|
||||
parsed := parseEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
|
||||
parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
|
@ -33,8 +33,8 @@ func NewDiffPatchPost(ctx *context.Context) {
|
|||
defaultCommitMessage := ctx.Locale.TrString("repo.editor.patch")
|
||||
_, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{
|
||||
LastCommitID: parsed.form.LastCommit,
|
||||
OldBranch: ctx.Repo.BranchName,
|
||||
NewBranch: parsed.TargetBranchName,
|
||||
OldBranch: parsed.OldBranchName,
|
||||
NewBranch: parsed.NewBranchName,
|
||||
Message: parsed.GetCommitMessage(defaultCommitMessage),
|
||||
Content: strings.ReplaceAll(parsed.form.Content.Value(), "\r\n", "\n"),
|
||||
Author: parsed.GitCommitter,
|
||||
|
|
@ -44,7 +44,7 @@ func NewDiffPatchPost(ctx *context.Context) {
|
|||
err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch")
|
||||
}
|
||||
if err != nil {
|
||||
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
|
||||
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
||||
return
|
||||
}
|
||||
redirectForCommitChoice(ctx, parsed, parsed.form.TreePath)
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ func CherryPick(ctx *context.Context) {
|
|||
|
||||
func CherryPickPost(ctx *context.Context) {
|
||||
fromCommitID := ctx.PathParam("sha")
|
||||
parsed := parseEditorCommitSubmittedForm[*forms.CherryPickForm](ctx)
|
||||
parsed := prepareEditorCommitSubmittedForm[*forms.CherryPickForm](ctx)
|
||||
if ctx.Written() {
|
||||
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))
|
||||
opts := &files.ApplyDiffPatchOptions{
|
||||
LastCommitID: parsed.form.LastCommit,
|
||||
OldBranch: ctx.Repo.BranchName,
|
||||
NewBranch: parsed.TargetBranchName,
|
||||
OldBranch: parsed.OldBranchName,
|
||||
NewBranch: parsed.NewBranchName,
|
||||
Message: parsed.GetCommitMessage(defaultCommitMessage),
|
||||
Author: parsed.GitCommitter,
|
||||
Committer: parsed.GitCommitter,
|
||||
|
|
@ -78,7 +78,7 @@ func CherryPickPost(ctx *context.Context) {
|
|||
}
|
||||
}
|
||||
if err != nil {
|
||||
editorHandleFileOperationError(ctx, parsed.TargetBranchName, err)
|
||||
editorHandleFileOperationError(ctx, parsed.NewBranchName, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -11,9 +11,11 @@ import (
|
|||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
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/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
context_service "code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
|
|
@ -83,3 +85,26 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
|
|||
}
|
||||
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),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
Name: form.RepoName,
|
||||
Description: form.Description,
|
||||
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 {
|
||||
ctx.Data["Err_RepoName"] = true
|
||||
switch {
|
||||
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)
|
||||
ctx.JSONError(msg)
|
||||
case repo_model.IsErrRepoAlreadyExist(err):
|
||||
|
|
@ -224,9 +232,7 @@ func ForkPost(ctx *context.Context) {
|
|||
default:
|
||||
ctx.ServerError("ForkPost", err)
|
||||
}
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
|
||||
ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
|
||||
return repo
|
||||
}
|
||||
|
|
|
|||
|
|
@ -290,7 +290,7 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
|
|||
|
||||
func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) {
|
||||
// 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
|
||||
}
|
||||
|
||||
|
|
@ -302,7 +302,9 @@ func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) {
|
|||
}
|
||||
|
||||
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["CanDeleteFile"] = true
|
||||
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
if !fInfo.isLFSFile && ctx.Repo.CanEnableEditor(ctx, ctx.Doer) {
|
||||
if !fInfo.isLFSFile && ctx.Repo.Repository.CanEnableEditor() {
|
||||
ctx.Data["CanEditReadmeFile"] = true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -749,6 +749,16 @@ func registerWebRoutes(m *web.Router) {
|
|||
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.Get("", admin.Emails)
|
||||
m.Post("/activate", admin.ActivateEmail)
|
||||
|
|
@ -1313,23 +1323,35 @@ func registerWebRoutes(m *web.Router) {
|
|||
}, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived())
|
||||
// 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.Post("/_preview/*", repo.DiffPreviewPost)
|
||||
m.Combo("/{editor_action:_edit}/*").Get(repo.EditFile).
|
||||
Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost)
|
||||
m.Combo("/{editor_action:_new}/*").Get(repo.EditFile).
|
||||
Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost)
|
||||
m.Combo("/_delete/*").Get(repo.DeleteFile).
|
||||
Post(web.Bind(forms.DeleteRepoFileForm{}), repo.DeleteFilePost)
|
||||
m.Combo("/_upload/*", repo.MustBeAbleToUpload).Get(repo.UploadFile).
|
||||
Post(web.Bind(forms.UploadRepoFileForm{}), repo.UploadFilePost)
|
||||
m.Combo("/_diffpatch/*").Get(repo.NewDiffPatch).
|
||||
Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost)
|
||||
m.Combo("/_cherrypick/{sha:([a-f0-9]{7,64})}/*").Get(repo.CherryPick).
|
||||
Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost)
|
||||
}, context.RepoRefByType(git.RefTypeBranch), context.CanWriteToBranch(), repo.WebGitOperationCommonData)
|
||||
// "GET" requests only need "code reader" permission, "POST" requests need "code writer" permission.
|
||||
// Because reader can "fork and edit"
|
||||
canWriteToBranch := context.CanWriteToBranch()
|
||||
m.Post("/_preview/*", repo.DiffPreviewPost) // read-only, fine with "code reader"
|
||||
m.Post("/_fork/*", repo.ForkToEditPost) // read-only, fork to own repo, fine with "code reader"
|
||||
|
||||
// the path params are used in PrepareCommitFormOptions to construct the correct form action URL
|
||||
m.Combo("/{editor_action:_edit}/*").
|
||||
Get(repo.EditFile).
|
||||
Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost)
|
||||
m.Combo("/{editor_action:_new}/*").
|
||||
Get(repo.EditFile).
|
||||
Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost)
|
||||
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.Post("/upload-file", repo.UploadFileToServer)
|
||||
m.Post("/upload-remove", repo.RemoveUploadFileFromServer)
|
||||
|
|
|
|||
|
|
@ -71,11 +71,6 @@ func (r *Repository) CanWriteToBranch(ctx context.Context, user *user_model.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.
|
||||
func (r *Repository) CanCreateBranch() bool {
|
||||
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
|
||||
EditorEnabled bool
|
||||
UserCanPush bool
|
||||
RequireSigned bool
|
||||
WillSign bool
|
||||
|
|
@ -106,51 +105,84 @@ type CommitFormBehaviors struct {
|
|||
CanCreateBasePullRequest bool
|
||||
}
|
||||
|
||||
func (r *Repository) PrepareCommitFormBehaviors(ctx *Context, doer *user_model.User) (*CommitFormBehaviors, error) {
|
||||
protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, r.Repository.ID, r.BranchName)
|
||||
func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *repo_model.Repository, doerRepoPerm access_model.Permission, refName git.RefName) (*CommitFormOptions, error) {
|
||||
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 {
|
||||
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)
|
||||
|
||||
canEnableEditor := r.CanEnableEditor(ctx, doer)
|
||||
canCommit := canEnableEditor && userCanPush
|
||||
if requireSigned {
|
||||
canCommit = canCommit && sign
|
||||
}
|
||||
wontSignReason := ""
|
||||
protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, targetRepo.ID, branchName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
canPushWithProtection := true
|
||||
protectionRequireSigned := false
|
||||
if protectedBranch != nil {
|
||||
protectedBranch.Repo = targetRepo
|
||||
canPushWithProtection = protectedBranch.CanUserPush(ctx, doer)
|
||||
protectionRequireSigned = protectedBranch.RequireSignedCommits
|
||||
}
|
||||
|
||||
willSign, signKeyID, _, err := asymkey_service.SignCRUDAction(ctx, targetRepo.RepoPath(), doer, targetRepo.RepoPath(), refName.String())
|
||||
wontSignReason := ""
|
||||
if asymkey_service.IsErrWontSign(err) {
|
||||
wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason)
|
||||
err = nil
|
||||
} else {
|
||||
wontSignReason = "error"
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
canCreateBasePullRequest := ctx.Repo.Repository.BaseRepo != nil && ctx.Repo.Repository.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests)
|
||||
canCreatePullRequest := ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypePullRequests) || canCreateBasePullRequest
|
||||
canCommitToBranch := !submitToForkedRepo /* same repo */ && targetRepo.CanEnableEditor() && canPushWithProtection
|
||||
if protectionRequireSigned {
|
||||
canCommitToBranch = canCommitToBranch && willSign
|
||||
}
|
||||
|
||||
return &CommitFormBehaviors{
|
||||
CanCommitToBranch: canCommit,
|
||||
EditorEnabled: canEnableEditor,
|
||||
UserCanPush: userCanPush,
|
||||
RequireSigned: requireSigned,
|
||||
WillSign: sign,
|
||||
SigningKey: keyID,
|
||||
canCreateBasePullRequest := targetRepo.BaseRepo != nil && targetRepo.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests)
|
||||
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,
|
||||
|
||||
CanCreatePullRequest: canCreatePullRequest,
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/reqctx"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"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["UploadMaxFiles"] = setting.Attachment.MaxFiles
|
||||
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:
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,19 @@ type AdminCreateUserForm struct {
|
|||
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
|
||||
func (f *AdminCreateUserForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
|
|
|
|||
|
|
@ -38,20 +38,23 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus,
|
|||
}
|
||||
|
||||
requiredCommitStatuses := make([]*git_model.CommitStatus, 0, len(commitStatuses))
|
||||
allRequiredContextsMatched := true
|
||||
for _, gp := range requiredContextsGlob {
|
||||
requiredContextMatched := false
|
||||
for _, commitStatus := range commitStatuses {
|
||||
if gp.Match(commitStatus.Context) {
|
||||
requiredCommitStatuses = append(requiredCommitStatuses, commitStatus)
|
||||
break
|
||||
requiredContextMatched = true
|
||||
}
|
||||
}
|
||||
allRequiredContextsMatched = allRequiredContextsMatched && requiredContextMatched
|
||||
}
|
||||
if len(requiredCommitStatuses) == 0 {
|
||||
return commitstatus.CommitStatusPending
|
||||
}
|
||||
|
||||
returnedStatus := git_model.CalcCommitStatus(requiredCommitStatuses).State
|
||||
if len(requiredCommitStatuses) == len(requiredContexts) {
|
||||
if allRequiredContextsMatched {
|
||||
return returnedStatus
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,15 @@ func TestMergeRequiredContextsCommitStatus(t *testing.T) {
|
|||
requiredContexts: []string{"Build*", "Build 2t*"},
|
||||
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{
|
||||
{Context: "Build 1", State: commitstatus.CommitStatusSuccess},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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" .}}
|
||||
|
|
@ -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" .}}
|
||||
|
|
@ -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" .}}
|
||||
|
|
@ -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" .}}
|
||||
|
|
@ -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" .}}
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
</a>
|
||||
</div>
|
||||
</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>
|
||||
<div class="menu">
|
||||
<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">
|
||||
{{ctx.Locale.Tr "admin.users"}}
|
||||
</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">
|
||||
{{ctx.Locale.Tr "admin.emails"}}
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -40,7 +40,12 @@
|
|||
{{svg "octicon-kebab-horizontal"}}
|
||||
<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>
|
||||
{{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"
|
||||
data-url="{{$run.Link}}/delete"
|
||||
data-modal-confirm="{{ctx.Locale.Tr "actions.runs.delete.description"}}"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
data-actions-url="{{.ActionsURL}}"
|
||||
|
||||
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-all="{{ctx.Locale.Tr "rerun_all"}}"
|
||||
data-locale-runs-scheduled="{{ctx.Locale.Tr "actions.runs.scheduled"}}"
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@
|
|||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
{{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}}
|
||||
{{template "repo/editor/common_top" .}}
|
||||
<input type="hidden" name="revert" value="{{if eq .CherryPickType "revert"}}true{{else}}false{{end}}">
|
||||
<div class="repo-editor-header">
|
||||
<div class="breadcrumb">
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<div class="commit-form-wrapper">
|
||||
{{ctx.AvatarUtils.Avatar .SignedUser 40 "commit-avatar"}}
|
||||
<div class="commit-form">
|
||||
<h3>{{- if .CommitFormBehaviors.WillSign}}
|
||||
<span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CommitFormBehaviors.SigningKey}}">{{svg "octicon-lock" 24}}</span>
|
||||
<h3>{{- if .CommitFormOptions.WillSign}}
|
||||
<span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CommitFormOptions.SigningKey}}">{{svg "octicon-lock" 24}}</span>
|
||||
{{ctx.Locale.Tr "repo.editor.commit_signed_changes"}}
|
||||
{{- 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"}}
|
||||
{{- end}}</h3>
|
||||
<div class="field">
|
||||
|
|
@ -22,17 +22,17 @@
|
|||
</div>
|
||||
<div class="quick-pull-choice js-quick-pull-choice">
|
||||
<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}}>
|
||||
<label>
|
||||
{{svg "octicon-git-commit"}}
|
||||
{{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">
|
||||
{{ctx.Locale.Tr "repo.editor.no_commit_to_branch"}}
|
||||
<ul>
|
||||
{{if not .CommitFormBehaviors.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 not .CommitFormOptions.UserCanPush}}<li>{{ctx.Locale.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}}
|
||||
{{if and .CommitFormOptions.RequireSigned (not .CommitFormOptions.WillSign)}}<li>{{ctx.Locale.Tr "repo.editor.require_signed_commit"}}</li>{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -42,14 +42,14 @@
|
|||
{{if and (not .Repository.IsEmpty) (not .IsEditingFileOnly)}}
|
||||
<div class="field">
|
||||
<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}}>
|
||||
{{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}}>
|
||||
{{end}}
|
||||
<label>
|
||||
{{svg "octicon-git-pull-request"}}
|
||||
{{if .CommitFormBehaviors.CanCreatePullRequest}}
|
||||
{{if .CommitFormOptions.CanCreatePullRequest}}
|
||||
{{ctx.Locale.Tr "repo.editor.create_new_branch"}}
|
||||
{{else}}
|
||||
{{ctx.Locale.Tr "repo.editor.create_new_branch_np"}}
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
|
|
@ -3,8 +3,9 @@
|
|||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
{{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}}
|
||||
{{template "repo/editor/common_top" .}}
|
||||
{{template "repo/editor/commit_form" .}}
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@
|
|||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
{{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-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
|
||||
>
|
||||
{{.CsrfTokenHtml}}
|
||||
{{template "repo/editor/common_top" .}}
|
||||
<div class="repo-editor-header">
|
||||
{{template "repo/editor/common_breadcrumb" .}}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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" .}}
|
||||
|
|
@ -3,11 +3,12 @@
|
|||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
{{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-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
|
||||
>
|
||||
{{.CsrfTokenHtml}}
|
||||
{{template "repo/editor/common_top" .}}
|
||||
<div class="repo-editor-header">
|
||||
<div class="breadcrumb">
|
||||
{{ctx.Locale.Tr "repo.editor.patching"}}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,9 @@
|
|||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
{{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}}
|
||||
{{template "repo/editor/common_top" .}}
|
||||
<div class="repo-editor-header">
|
||||
{{template "repo/editor/common_breadcrumb" .}}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
{{end}}
|
||||
|
||||
{{if and .CanWriteCode .RefFullName.IsBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}}
|
||||
<button class="ui dropdown basic compact jump button"{{if not .Repository.CanEnableEditor}} disabled{{end}}>
|
||||
{{if and .RefFullName.IsBranch (not .IsViewFile)}}
|
||||
<button class="ui dropdown basic compact jump button repo-add-file" {{if not .Repository.CanEnableEditor}}disabled{{end}}>
|
||||
{{ctx.Locale.Tr "repo.editor.add_file"}}
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="menu">
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
|
@ -19,178 +20,164 @@ import (
|
|||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCreateFile(t *testing.T) {
|
||||
func TestEditor(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
session := loginUser(t, "user2")
|
||||
testCreateFile(t, session, "user2", "repo1", "master", "test.txt", "Content")
|
||||
sessionUser2 := loginUser(t, "user2")
|
||||
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) {
|
||||
// Request editor page
|
||||
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,
|
||||
testEditorActionEdit(t, session, user, repo, "_new", branch, "", map[string]string{
|
||||
"tree_path": filePath,
|
||||
"content": content,
|
||||
"commit_choice": "direct",
|
||||
})
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.NotEmpty(t, test.RedirectURL(resp))
|
||||
}
|
||||
|
||||
func TestCreateFileOnProtectedBranch(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
func testEditorProtectedBranch(t *testing.T) {
|
||||
session := loginUser(t, "user2")
|
||||
|
||||
csrf := GetUserCSRFToken(t, session)
|
||||
// Change master branch to protected
|
||||
// Change the "master" branch to "protected"
|
||||
req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{
|
||||
"_csrf": csrf,
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
"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
|
||||
req = NewRequest(t, "GET", "/user2/repo1/_new/master/")
|
||||
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", "/user2/repo1/_new/master/", map[string]string{
|
||||
"_csrf": doc.GetCSRF(),
|
||||
"last_commit": lastCommit,
|
||||
"tree_path": "test.txt",
|
||||
"content": "Content",
|
||||
"commit_choice": "direct",
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
// Try to commit a file to the "master" branch and it should fail
|
||||
resp := testEditorActionPostRequest(t, session, "/user2/repo1/_new/master/", map[string]string{"tree_path": "test-protected-branch.txt", "commit_choice": "direct"})
|
||||
assert.Equal(t, http.StatusBadRequest, resp.Code)
|
||||
assert.Equal(t, `Cannot commit to protected branch "master".`, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
|
||||
}
|
||||
|
||||
func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePath, newContent string) *httptest.ResponseRecorder {
|
||||
// Get to the 'edit this file' page
|
||||
req := NewRequest(t, "GET", path.Join(user, repo, "_edit", branch, filePath))
|
||||
func testEditorActionPostRequest(t *testing.T, session *TestSession, requestPath string, params map[string]string) *httptest.ResponseRecorder {
|
||||
req := NewRequest(t, "GET", requestPath)
|
||||
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{
|
||||
form := map[string]string{
|
||||
"_csrf": htmlDoc.GetCSRF(),
|
||||
"last_commit": lastCommit,
|
||||
"tree_path": filePath,
|
||||
"content": newContent,
|
||||
"commit_choice": "direct",
|
||||
},
|
||||
)
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
"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))
|
||||
|
||||
// Verify the change
|
||||
req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", branch, filePath))
|
||||
req := NewRequest(t, "GET", path.Join(user, repo, "raw/branch", newBranchName, params["tree_path"]))
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, newContent, resp.Body.String())
|
||||
|
||||
assert.Equal(t, params["content"], 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)
|
||||
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",
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
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,
|
||||
},
|
||||
)
|
||||
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) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
func testEditorDiffPreview(t *testing.T) {
|
||||
session := loginUser(t, "user2")
|
||||
testEditFileToNewBranch(t, session, "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited)\n")
|
||||
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 TestWebGitCommitEmail(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
|
||||
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)
|
||||
|
||||
|
|
@ -338,5 +325,131 @@ index 0000000000..bbbbbbbbbb
|
|||
// 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))
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ func (doc *HTMLDoc) GetCSRF() string {
|
|||
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) {
|
||||
sel := doc.doc.Find(selector)
|
||||
switch v := any(checkExists).(type) {
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) {
|
|||
session := loginUser(t, "user1")
|
||||
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
|
||||
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")
|
||||
req := NewRequestWithValues(t, "POST", url,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
// TODO: Switch to upstream after https://github.com/razorness/vue3-calendar-heatmap/pull/34 is merged
|
||||
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';
|
||||
|
||||
defineProps<{
|
||||
|
|
@ -24,7 +24,7 @@ const colorRange = [
|
|||
'var(--color-primary-dark-4)',
|
||||
];
|
||||
|
||||
const endDate = ref(new Date());
|
||||
const endDate = shallowRef(new Date());
|
||||
|
||||
onMounted(() => {
|
||||
// work around issue with first legend color being rendered twice and legend cut off
|
||||
|
|
|
|||
|
|
@ -2,16 +2,16 @@
|
|||
import {SvgIcon} from '../svg.ts';
|
||||
import {GET} from '../modules/fetch.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';
|
||||
|
||||
const {appSubUrl, i18n} = window.config;
|
||||
|
||||
const loading = ref(false);
|
||||
const issue = ref(null);
|
||||
const renderedLabels = ref('');
|
||||
const loading = shallowRef(false);
|
||||
const issue = shallowRef(null);
|
||||
const renderedLabels = shallowRef('');
|
||||
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 body = computed(() => {
|
||||
|
|
@ -22,7 +22,7 @@ const body = computed(() => {
|
|||
return body;
|
||||
});
|
||||
|
||||
const root = ref<HTMLElement | null>(null);
|
||||
const root = useTemplateRef('root');
|
||||
|
||||
onMounted(() => {
|
||||
root.value.addEventListener('ce-load-context-popup', (e: CustomEventInit<IssuePathInfo>) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts" setup>
|
||||
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';
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
@ -8,7 +8,7 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const store = diffTreeStore();
|
||||
const collapsed = ref(props.item.IsViewed);
|
||||
const collapsed = shallowRef(props.item.IsViewed);
|
||||
|
||||
function getIconForDiffStatus(pType: DiffStatus) {
|
||||
const diffTypes: Record<DiffStatus, { name: SvgName, classes: Array<string> }> = {
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
<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 {toggleElem} from '../utils/dom.ts';
|
||||
|
||||
const {csrfToken, pageData} = window.config;
|
||||
|
||||
const mergeForm = ref(pageData.pullRequestMergeForm);
|
||||
const mergeForm = pageData.pullRequestMergeForm;
|
||||
|
||||
const mergeTitleFieldValue = ref('');
|
||||
const mergeMessageFieldValue = ref('');
|
||||
const deleteBranchAfterMerge = ref(false);
|
||||
const autoMergeWhenSucceed = ref(false);
|
||||
const mergeTitleFieldValue = shallowRef('');
|
||||
const mergeMessageFieldValue = shallowRef('');
|
||||
const deleteBranchAfterMerge = shallowRef(false);
|
||||
const autoMergeWhenSucceed = shallowRef(false);
|
||||
|
||||
const mergeStyle = ref('');
|
||||
const mergeStyleDetail = ref({
|
||||
const mergeStyle = shallowRef('');
|
||||
const mergeStyleDetail = shallowRef({
|
||||
hideMergeMessageTexts: false,
|
||||
textDoMerge: '',
|
||||
mergeTitleFieldText: '',
|
||||
|
|
@ -21,33 +21,33 @@ const mergeStyleDetail = ref({
|
|||
hideAutoMerge: false,
|
||||
});
|
||||
|
||||
const mergeStyleAllowedCount = ref(0);
|
||||
const mergeStyleAllowedCount = shallowRef(0);
|
||||
|
||||
const showMergeStyleMenu = ref(false);
|
||||
const showActionForm = ref(false);
|
||||
const showMergeStyleMenu = shallowRef(false);
|
||||
const showActionForm = shallowRef(false);
|
||||
|
||||
const mergeButtonStyleClass = computed(() => {
|
||||
if (mergeForm.value.allOverridableChecksOk) return 'primary';
|
||||
if (mergeForm.allOverridableChecksOk) return 'primary';
|
||||
return autoMergeWhenSucceed.value ? 'primary' : 'red';
|
||||
});
|
||||
|
||||
const forceMerge = computed(() => {
|
||||
return mergeForm.value.canMergeNow && !mergeForm.value.allOverridableChecksOk;
|
||||
return mergeForm.canMergeNow && !mergeForm.allOverridableChecksOk;
|
||||
});
|
||||
|
||||
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]')) {
|
||||
toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val);
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
if (!mergeStyle) mergeStyle = mergeForm.value.mergeStyles.find((e: any) => e.allowed)?.name;
|
||||
switchMergeStyle(mergeStyle, !mergeForm.value.canMergeNow);
|
||||
let mergeStyle = mergeForm.mergeStyles.find((e: any) => e.allowed && e.name === mergeForm.defaultMergeStyle)?.name;
|
||||
if (!mergeStyle) mergeStyle = mergeForm.mergeStyles.find((e: any) => e.allowed)?.name;
|
||||
switchMergeStyle(mergeStyle, !mergeForm.canMergeNow);
|
||||
|
||||
document.addEventListener('mouseup', hideMergeStyleMenu);
|
||||
});
|
||||
|
|
@ -63,7 +63,7 @@ function hideMergeStyleMenu() {
|
|||
function toggleActionForm(show: boolean) {
|
||||
showActionForm.value = show;
|
||||
if (!show) return;
|
||||
deleteBranchAfterMerge.value = mergeForm.value.defaultDeleteBranchAfterMerge;
|
||||
deleteBranchAfterMerge.value = mergeForm.defaultDeleteBranchAfterMerge;
|
||||
mergeTitleFieldValue.value = mergeStyleDetail.value.mergeTitleFieldText;
|
||||
mergeMessageFieldValue.value = mergeStyleDetail.value.mergeMessageFieldText;
|
||||
}
|
||||
|
|
@ -74,7 +74,7 @@ function switchMergeStyle(name: string, autoMerge = false) {
|
|||
}
|
||||
|
||||
function clearMergeMessage() {
|
||||
mergeMessageFieldValue.value = mergeForm.value.defaultMergeMessage;
|
||||
mergeMessageFieldValue.value = mergeForm.defaultMergeMessage;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<script lang="ts" setup>
|
||||
// @ts-expect-error - module exports no types
|
||||
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',
|
||||
textColor: 'black',
|
||||
textAltColor: 'white',
|
||||
|
|
@ -41,8 +41,8 @@ const graphWidth = computed(() => {
|
|||
return activityTopAuthors.length * 40;
|
||||
});
|
||||
|
||||
const styleElement = ref<HTMLElement | null>(null);
|
||||
const altStyleElement = ref<HTMLElement | null>(null);
|
||||
const styleElement = useTemplateRef('styleElement');
|
||||
const altStyleElement = useTemplateRef('altStyleElement');
|
||||
|
||||
onMounted(() => {
|
||||
const refStyle = window.getComputedStyle(styleElement.value);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import {
|
|||
import {chartJsColors} from '../utils/color.ts';
|
||||
import {sleep} from '../utils.ts';
|
||||
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;
|
||||
|
||||
|
|
@ -47,10 +47,10 @@ defineProps<{
|
|||
};
|
||||
}>();
|
||||
|
||||
const isLoading = ref(false);
|
||||
const errorText = ref('');
|
||||
const repoLink = ref(pageData.repoLink || []);
|
||||
const data = ref<DayData[]>([]);
|
||||
const isLoading = shallowRef(false);
|
||||
const errorText = shallowRef('');
|
||||
const repoLink = pageData.repoLink;
|
||||
const data = shallowRef<DayData[]>([]);
|
||||
|
||||
onMounted(() => {
|
||||
fetchGraphData();
|
||||
|
|
@ -61,7 +61,7 @@ async function fetchGraphData() {
|
|||
try {
|
||||
let response: Response;
|
||||
do {
|
||||
response = await GET(`${repoLink.value}/activity/code-frequency/data`);
|
||||
response = await GET(`${repoLink}/activity/code-frequency/data`);
|
||||
if (response.status === 202) {
|
||||
await sleep(1000); // wait for 1 second before retrying
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import {
|
|||
import {chartJsColors} from '../utils/color.ts';
|
||||
import {sleep} from '../utils.ts';
|
||||
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;
|
||||
|
||||
|
|
@ -43,9 +43,9 @@ defineProps<{
|
|||
};
|
||||
}>();
|
||||
|
||||
const isLoading = ref(false);
|
||||
const errorText = ref('');
|
||||
const repoLink = ref(pageData.repoLink || []);
|
||||
const isLoading = shallowRef(false);
|
||||
const errorText = shallowRef('');
|
||||
const repoLink = pageData.repoLink;
|
||||
const data = ref<DayData[]>([]);
|
||||
|
||||
onMounted(() => {
|
||||
|
|
@ -57,7 +57,7 @@ async function fetchGraphData() {
|
|||
try {
|
||||
let response: Response;
|
||||
do {
|
||||
response = await GET(`${repoLink.value}/activity/recent-commits/data`);
|
||||
response = await GET(`${repoLink}/activity/recent-commits/data`);
|
||||
if (response.status === 202) {
|
||||
await sleep(1000); // wait for 1 second before retrying
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<script lang="ts" setup>
|
||||
import ViewFileTreeItem from './ViewFileTreeItem.vue';
|
||||
import {onMounted, ref} from 'vue';
|
||||
import {onMounted, useTemplateRef} from 'vue';
|
||||
import {createViewFileTreeStore} from './ViewFileTreeStore.ts';
|
||||
|
||||
const elRoot = ref<HTMLElement | null>(null);
|
||||
const elRoot = useTemplateRef('elRoot');
|
||||
|
||||
const props = defineProps({
|
||||
repoLink: {type: String, required: true},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
import {SvgIcon} from '../svg.ts';
|
||||
import {isPlainClick} from '../utils/dom.ts';
|
||||
import {ref} from 'vue';
|
||||
import {shallowRef} from 'vue';
|
||||
import {type createViewFileTreeStore} from './ViewFileTreeStore.ts';
|
||||
|
||||
type Item = {
|
||||
|
|
@ -20,9 +20,9 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const store = props.store;
|
||||
const isLoading = ref(false);
|
||||
const children = ref(props.item.children);
|
||||
const collapsed = ref(!props.item.children);
|
||||
const isLoading = shallowRef(false);
|
||||
const children = shallowRef(props.item.children);
|
||||
const collapsed = shallowRef(!props.item.children);
|
||||
|
||||
const doLoadChildren = async () => {
|
||||
collapsed.value = !collapsed.value;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {confirmModal} from './comp/ConfirmModal.ts';
|
|||
import type {RequestOpts} from '../types.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"
|
||||
// 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) {
|
||||
const showErrorForResponse = (code: number, message: string) => {
|
||||
showErrorToast(`Error ${code || 'request'}: ${message}`);
|
||||
};
|
||||
|
||||
let respStatus = 0;
|
||||
let respText = '';
|
||||
try {
|
||||
hideToastsAll();
|
||||
const resp = await request(url, opt);
|
||||
if (resp.status === 200) {
|
||||
let {redirect} = await resp.json();
|
||||
respStatus = resp.status;
|
||||
respText = await resp.text();
|
||||
const respJson = JSON.parse(respText);
|
||||
if (respStatus === 200) {
|
||||
let {redirect} = respJson;
|
||||
redirect = redirect || actionElem.getAttribute('data-redirect');
|
||||
ignoreAreYouSure(actionElem); // ignore the areYouSure check before reloading
|
||||
if (redirect) {
|
||||
|
|
@ -38,22 +47,19 @@ async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: R
|
|||
return;
|
||||
}
|
||||
|
||||
if (resp.status >= 400 && resp.status < 500) {
|
||||
const data = await resp.json();
|
||||
if (respStatus >= 400 && respStatus < 500 && respJson?.errorMessage) {
|
||||
// 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.
|
||||
if (data.errorMessage) {
|
||||
showErrorToast(data.errorMessage, {useHtmlBody: data.renderFormat === 'html'});
|
||||
showErrorToast(respJson.errorMessage, {useHtmlBody: respJson.renderFormat === 'html'});
|
||||
} else {
|
||||
showErrorToast(`server error: ${resp.status}`);
|
||||
}
|
||||
} else {
|
||||
showErrorToast(`server error: ${resp.status}`);
|
||||
showErrorForResponse(respStatus, respText);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.name !== 'AbortError') {
|
||||
console.error('error when doRequest', e);
|
||||
showErrorToast(`${i18n.network_error} ${e}`);
|
||||
if (e.name === 'SyntaxError') {
|
||||
showErrorForResponse(respStatus, (respText || '').substring(0, 100));
|
||||
} else if (e.name !== 'AbortError') {
|
||||
console.error('fetchActionDoRequest error', e);
|
||||
showErrorForResponse(respStatus, `${e}`);
|
||||
}
|
||||
}
|
||||
actionElem.classList.remove('is-loading', 'loading-icon-2px');
|
||||
|
|
|
|||
Loading…
Reference in New Issue