mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-21 09:06:46 +09:00
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76e88b30c0 | ||
|
|
82536a3ede | ||
|
|
c52e7c0cd2 | ||
|
|
738ff1d65a | ||
|
|
52eb515751 | ||
|
|
d7d658a8e9 | ||
|
|
5265238726 | ||
|
|
35fa0400e9 | ||
|
|
edd0c0e2ce | ||
|
|
e645afbb3f | ||
|
|
7610336d74 | ||
|
|
3701fcec84 | ||
|
|
f8dd2dd4cc | ||
|
|
ba4f8fa246 |
No files matched your search
@@ -1218,8 +1218,9 @@ LEVEL = Info
|
|||||||
;; In default merge messages only include approvers who are official
|
;; In default merge messages only include approvers who are official
|
||||||
;DEFAULT_MERGE_MESSAGE_OFFICIAL_APPROVERS_ONLY = true
|
;DEFAULT_MERGE_MESSAGE_OFFICIAL_APPROVERS_ONLY = true
|
||||||
;;
|
;;
|
||||||
;; In default squash-merge messages include the commit message of all commits comprising the pull request.
|
;; Set default squash merge commit message body, valid options: PR_BODY (pull request description), COMMIT_MESSAGES (commit messages), BLANK (none)
|
||||||
;POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES = false
|
;; Repositories store the current value when created or when their settings are next saved, and can change it there.
|
||||||
|
;DEFAULT_SQUASH_MERGE_MESSAGE = PR_BODY
|
||||||
;;
|
;;
|
||||||
;; Add co-authored-by and co-committed-by trailers if committer does not match author
|
;; Add co-authored-by and co-committed-by trailers if committer does not match author
|
||||||
;ADD_CO_COMMITTER_TRAILERS = true
|
;ADD_CO_COMMITTER_TRAILERS = true
|
||||||
@@ -1433,11 +1434,14 @@ LEVEL = Info
|
|||||||
;; The icon theme for folders (basic/material)
|
;; The icon theme for folders (basic/material)
|
||||||
;FOLDER_ICON_THEME = basic
|
;FOLDER_ICON_THEME = basic
|
||||||
;;
|
;;
|
||||||
;; All available reactions users can choose on issues/prs and comments.
|
;; All available reactions users can choose on issues, pull requests and comments.
|
||||||
;; Values can be emoji alias (:smile:) or a unicode emoji.
|
;; Values can be emoji alias (:smile:) or a unicode emoji.
|
||||||
;; For custom reactions, add a tightly cropped square image to public/assets/img/emoji/reaction_name.png
|
;; For custom reactions, add a tightly cropped square image to public/assets/img/emoji/reaction_name.png
|
||||||
;REACTIONS = +1, -1, laugh, hooray, confused, heart, rocket, eyes
|
;REACTIONS = +1, -1, laugh, hooray, confused, heart, rocket, eyes
|
||||||
;;
|
;;
|
||||||
|
;; Reactions available for releases
|
||||||
|
;;RELEASE_REACTIONS = +1, laugh, hooray, heart, rocket, eyes
|
||||||
|
;;
|
||||||
;; Change the number of users that are displayed in reactions tooltip (triggered by mouse hover).
|
;; Change the number of users that are displayed in reactions tooltip (triggered by mouse hover).
|
||||||
;REACTION_MAX_USER_NUM = 10
|
;REACTION_MAX_USER_NUM = 10
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ module gitea.dev
|
|||||||
|
|
||||||
go 1.27
|
go 1.27
|
||||||
|
|
||||||
toolchain go1.27.2
|
toolchain go1.27.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
connectrpc.com/connect v1.21.0
|
connectrpc.com/connect v1.21.0
|
||||||
|
|||||||
@@ -439,6 +439,7 @@ func prepareMigrationTasks() []*migration {
|
|||||||
newMigration(360, "Move branch protection settings to JSON", v29.MoveBranchProtectionConfigToJSON),
|
newMigration(360, "Move branch protection settings to JSON", v29.MoveBranchProtectionConfigToJSON),
|
||||||
newMigration(361, "Drop legacy commit_id column from comment", v29.DropCommentCommitID),
|
newMigration(361, "Drop legacy commit_id column from comment", v29.DropCommentCommitID),
|
||||||
newMigration(362, "Add reusable_workflow_ref column to action_run_job", v29.AddReusableWorkflowRefToActionRunJob),
|
newMigration(362, "Add reusable_workflow_ref column to action_run_job", v29.AddReusableWorkflowRefToActionRunJob),
|
||||||
|
newMigration(363, "Add release_id to reaction table", v29.AddReleaseIDToReaction),
|
||||||
}
|
}
|
||||||
return preparedMigrations
|
return preparedMigrations
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v29
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.dev/modelmigration/base"
|
||||||
|
"gitea.dev/modules/timeutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddReleaseIDToReaction(_ctx context.Context, x base.EngineMigration) error {
|
||||||
|
// rows older than v123 can hold NULL, which the unique index doesn't treat as a duplicate of ''
|
||||||
|
if _, err := x.Exec("UPDATE reaction SET original_author = '' WHERE original_author IS NULL"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type Reaction struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
|
||||||
|
UserID int64 `xorm:"UNIQUE(s) NOT NULL"` // first unique column implies INDEX
|
||||||
|
IssueID int64 `xorm:"UNIQUE(s) INDEX(issue_comment) NOT NULL"`
|
||||||
|
CommentID int64 `xorm:"UNIQUE(s) INDEX(issue_comment) INDEX NOT NULL"`
|
||||||
|
ReleaseID int64 `xorm:"UNIQUE(s) INDEX NOT NULL DEFAULT(0)"`
|
||||||
|
OriginalAuthorID int64 `xorm:"UNIQUE(s) NOT NULL DEFAULT(0)"`
|
||||||
|
OriginalAuthor string `xorm:"UNIQUE(s) NOT NULL DEFAULT('')"`
|
||||||
|
Type string `xorm:"UNIQUE(s) NOT NULL"`
|
||||||
|
|
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||||
|
}
|
||||||
|
return x.Sync(new(Reaction))
|
||||||
|
}
|
||||||
@@ -725,10 +725,7 @@ func (c *Comment) LoadReactions(ctx context.Context, repo *repo_model.Repository
|
|||||||
if c.Reactions != nil {
|
if c.Reactions != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
c.Reactions, _, err = FindReactions(ctx, FindReactionsOptions{
|
c.Reactions, _, err = FindReactions(ctx, FindReactionsOptions{CommentID: c.ID})
|
||||||
IssueID: c.IssueID,
|
|
||||||
CommentID: c.ID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1210,7 +1207,7 @@ func DeleteComment(ctx context.Context, comment *Comment) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return DeleteReaction(ctx, &ReactionOptions{CommentID: comment.ID})
|
return DeleteCommentAllReactions(ctx, comment.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateCommentsMigrationsByType updates comments' migrations information via given git service type and original id and poster id
|
// UpdateCommentsMigrationsByType updates comments' migrations information via given git service type and original id and poster id
|
||||||
|
|||||||
@@ -225,13 +225,11 @@ func (issue *Issue) loadCommentsByType(ctx context.Context, tp CommentType) (err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (issue *Issue) loadReactions(ctx context.Context) (err error) {
|
func (issue *Issue) loadAllReactions(ctx context.Context) (err error) {
|
||||||
if issue.Reactions != nil {
|
if issue.Reactions != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
reactions, _, err := FindReactions(ctx, FindReactionsOptions{
|
reactions, _, err := FindReactions(ctx, FindReactionsOptions{IssueIDWithComments: issue.ID})
|
||||||
IssueID: issue.ID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -340,7 +338,7 @@ func (issue *Issue) LoadAttributes(ctx context.Context) (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return issue.loadReactions(ctx)
|
return issue.loadAllReactions(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsPinned returns if a Issue is pinned
|
// IsPinned returns if a Issue is pinned
|
||||||
|
|||||||
+84
-110
@@ -58,17 +58,20 @@ func (err ErrReactionAlreadyExist) Unwrap() error {
|
|||||||
return util.ErrAlreadyExist
|
return util.ErrAlreadyExist
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reaction represents a reactions on issues and comments.
|
// Reaction represents a reaction on an issue, comment or release.
|
||||||
type Reaction struct {
|
type Reaction struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
Type string `xorm:"INDEX UNIQUE(s) NOT NULL"`
|
|
||||||
IssueID int64 `xorm:"INDEX UNIQUE(s) NOT NULL"`
|
UserID int64 `xorm:"UNIQUE(s) NOT NULL"` // first unique column implies INDEX
|
||||||
CommentID int64 `xorm:"INDEX UNIQUE(s)"`
|
IssueID int64 `xorm:"UNIQUE(s) INDEX(issue_comment) NOT NULL"`
|
||||||
UserID int64 `xorm:"INDEX UNIQUE(s) NOT NULL"`
|
CommentID int64 `xorm:"UNIQUE(s) INDEX(issue_comment) INDEX NOT NULL"`
|
||||||
OriginalAuthorID int64 `xorm:"INDEX UNIQUE(s) NOT NULL DEFAULT(0)"`
|
ReleaseID int64 `xorm:"UNIQUE(s) INDEX NOT NULL DEFAULT(0)"`
|
||||||
OriginalAuthor string `xorm:"INDEX UNIQUE(s)"`
|
OriginalAuthorID int64 `xorm:"UNIQUE(s) NOT NULL DEFAULT(0)"`
|
||||||
User *user_model.User `xorm:"-"`
|
OriginalAuthor string `xorm:"UNIQUE(s) NOT NULL DEFAULT('')"`
|
||||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
Type string `xorm:"UNIQUE(s) NOT NULL"`
|
||||||
|
|
||||||
|
User *user_model.User `xorm:"-"`
|
||||||
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadUser load user of reaction
|
// LoadUser load user of reaction
|
||||||
@@ -108,53 +111,48 @@ func init() {
|
|||||||
// FindReactionsOptions describes the conditions to Find reactions
|
// FindReactionsOptions describes the conditions to Find reactions
|
||||||
type FindReactionsOptions struct {
|
type FindReactionsOptions struct {
|
||||||
db.ListOptions
|
db.ListOptions
|
||||||
IssueID int64
|
IssueIDOnly int64
|
||||||
CommentID int64
|
IssueIDWithComments int64
|
||||||
UserID int64
|
CommentID int64
|
||||||
Reaction string
|
ReleaseID int64
|
||||||
|
UserID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts *FindReactionsOptions) toConds() builder.Cond {
|
func (opts *FindReactionsOptions) toConds() builder.Cond {
|
||||||
// If Issue ID is set add to Query
|
// If Issue ID is set add to Query
|
||||||
cond := builder.NewCond()
|
cond := builder.NewCond()
|
||||||
if opts.IssueID > 0 {
|
if opts.IssueIDOnly > 0 {
|
||||||
cond = cond.And(builder.Eq{"reaction.issue_id": opts.IssueID})
|
cond = cond.And(builder.Eq{"reaction.issue_id": opts.IssueIDOnly}).And(builder.Expr("reaction.comment_id = 0"))
|
||||||
|
}
|
||||||
|
if opts.IssueIDWithComments > 0 {
|
||||||
|
cond = cond.And(builder.Eq{"reaction.issue_id": opts.IssueIDWithComments})
|
||||||
}
|
}
|
||||||
// If CommentID is > 0 add to Query
|
|
||||||
// If it is 0 Query ignore CommentID to select
|
|
||||||
// If it is -1 it explicit search of Issue Reactions where CommentID = 0
|
|
||||||
if opts.CommentID > 0 {
|
if opts.CommentID > 0 {
|
||||||
cond = cond.And(builder.Eq{"reaction.comment_id": opts.CommentID})
|
cond = cond.And(builder.Eq{"reaction.comment_id": opts.CommentID})
|
||||||
} else if opts.CommentID == -1 {
|
}
|
||||||
cond = cond.And(builder.Eq{"reaction.comment_id": 0})
|
if opts.ReleaseID > 0 {
|
||||||
|
cond = cond.And(builder.Eq{"reaction.release_id": opts.ReleaseID})
|
||||||
}
|
}
|
||||||
if opts.UserID > 0 {
|
if opts.UserID > 0 {
|
||||||
cond = cond.And(builder.Eq{
|
cond = cond.And(builder.Eq{"reaction.user_id": opts.UserID, "reaction.original_author_id": 0})
|
||||||
"reaction.user_id": opts.UserID,
|
|
||||||
"reaction.original_author_id": 0,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if opts.Reaction != "" {
|
if !cond.IsValid() {
|
||||||
cond = cond.And(builder.Eq{"reaction.type": opts.Reaction})
|
setting.PanicInDevOrTesting("FindReactionsOptions.toConds: no conditions specified")
|
||||||
|
cond = cond.And(builder.Expr("1=0"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return cond
|
return cond
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindCommentReactions returns a ReactionList of all reactions from an comment
|
// FindCommentReactions returns a ReactionList of all reactions from a comment
|
||||||
func FindCommentReactions(ctx context.Context, issueID, commentID int64) (ReactionList, int64, error) {
|
func FindCommentReactions(ctx context.Context, commentID int64) (ReactionList, int64, error) {
|
||||||
return FindReactions(ctx, FindReactionsOptions{
|
return FindReactions(ctx, FindReactionsOptions{CommentID: commentID})
|
||||||
IssueID: issueID,
|
|
||||||
CommentID: commentID,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindIssueReactions returns a ReactionList of all reactions from an issue
|
// FindIssueReactions returns a ReactionList of all reactions from an issue
|
||||||
func FindIssueReactions(ctx context.Context, issueID int64, listOptions db.ListOptions) (ReactionList, int64, error) {
|
func FindIssueReactions(ctx context.Context, issueID int64, listOptions db.ListOptions) (ReactionList, int64, error) {
|
||||||
return FindReactions(ctx, FindReactionsOptions{
|
return FindReactions(ctx, FindReactionsOptions{
|
||||||
ListOptions: listOptions,
|
ListOptions: listOptions,
|
||||||
IssueID: issueID,
|
IssueIDOnly: issueID,
|
||||||
CommentID: -1,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,37 +175,14 @@ func FindReactions(ctx context.Context, opts FindReactionsOptions) (ReactionList
|
|||||||
return reactions, count, err
|
return reactions, count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func createReaction(ctx context.Context, opts *ReactionOptions) (*Reaction, error) {
|
func FindReleaseReactions(ctx context.Context, releaseIDs ...int64) (ReactionList, error) {
|
||||||
reaction := &Reaction{
|
reactions := make(ReactionList, 0, 10)
|
||||||
Type: opts.Type,
|
err := db.GetEngine(ctx).
|
||||||
UserID: opts.DoerID,
|
In("release_id", releaseIDs).
|
||||||
IssueID: opts.IssueID,
|
In("`type`", setting.UI.ReleaseReactions).
|
||||||
CommentID: opts.CommentID,
|
Asc("created_unix", "id").
|
||||||
}
|
Find(&reactions)
|
||||||
findOpts := FindReactionsOptions{
|
return reactions, err
|
||||||
IssueID: opts.IssueID,
|
|
||||||
CommentID: opts.CommentID,
|
|
||||||
Reaction: opts.Type,
|
|
||||||
UserID: opts.DoerID,
|
|
||||||
}
|
|
||||||
if findOpts.CommentID == 0 {
|
|
||||||
// explicit search of Issue Reactions where CommentID = 0
|
|
||||||
findOpts.CommentID = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
existingR, _, err := FindReactions(ctx, findOpts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(existingR) > 0 {
|
|
||||||
return existingR[0], ErrReactionAlreadyExist{Reaction: opts.Type}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.Insert(ctx, reaction); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return reaction, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReactionOptions defines options for creating or deleting reactions
|
// ReactionOptions defines options for creating or deleting reactions
|
||||||
@@ -216,56 +191,63 @@ type ReactionOptions struct {
|
|||||||
DoerID int64
|
DoerID int64
|
||||||
IssueID int64
|
IssueID int64
|
||||||
CommentID int64
|
CommentID int64
|
||||||
|
ReleaseID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateReaction creates reaction for issue or comment.
|
// CreateReaction creates reaction for issue, comment or release.
|
||||||
func CreateReaction(ctx context.Context, opts *ReactionOptions) (*Reaction, error) {
|
func CreateReaction(ctx context.Context, allowed container.Set[string], opts *ReactionOptions) (*Reaction, error) {
|
||||||
if !setting.UI.ReactionsLookup.Contains(opts.Type) {
|
if !allowed.Contains(opts.Type) {
|
||||||
return nil, ErrForbiddenIssueReaction{opts.Type}
|
return nil, ErrForbiddenIssueReaction{opts.Type}
|
||||||
}
|
}
|
||||||
|
|
||||||
return db.WithTx2(ctx, func(ctx context.Context) (*Reaction, error) {
|
|
||||||
return createReaction(ctx, opts)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteReaction deletes reaction for issue or comment.
|
|
||||||
func DeleteReaction(ctx context.Context, opts *ReactionOptions) error {
|
|
||||||
reaction := &Reaction{
|
reaction := &Reaction{
|
||||||
Type: opts.Type,
|
Type: opts.Type,
|
||||||
UserID: opts.DoerID,
|
UserID: opts.DoerID,
|
||||||
IssueID: opts.IssueID,
|
IssueID: opts.IssueID,
|
||||||
CommentID: opts.CommentID,
|
CommentID: opts.CommentID,
|
||||||
|
ReleaseID: opts.ReleaseID,
|
||||||
}
|
}
|
||||||
|
|
||||||
sess := db.GetEngine(ctx).Where("original_author_id = 0")
|
err := db.Insert(ctx, reaction)
|
||||||
if opts.CommentID == -1 {
|
if err != nil {
|
||||||
reaction.CommentID = 0
|
if has, _ := db.GetEngine(ctx).Get(reaction); has {
|
||||||
sess.MustCols("comment_id")
|
return reaction, ErrReactionAlreadyExist{Reaction: opts.Type}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return reaction, nil
|
||||||
|
}
|
||||||
|
|
||||||
_, err := sess.Delete(reaction)
|
func DeleteIssueReaction(ctx context.Context, doerID, issueID int64, content string) error {
|
||||||
|
// here and below: the migration reactions have unrelated user_id (might be the doer?)
|
||||||
|
// so skip the deletion if the reaction is from an external user (original_author_id != 0)
|
||||||
|
sess := db.GetEngine(ctx).Where("issue_id = ? AND comment_id = 0 AND user_id = ? AND original_author_id = 0 AND `type` = ?", issueID, doerID, content)
|
||||||
|
_, err := sess.Delete(&Reaction{})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteIssueReaction deletes a reaction on issue.
|
func DeleteCommentReaction(ctx context.Context, doerID, issueID, commentID int64, content string) error {
|
||||||
func DeleteIssueReaction(ctx context.Context, doerID, issueID int64, content string) error {
|
sess := db.GetEngine(ctx).Where("issue_id = ? AND comment_id = ? AND user_id = ? AND original_author_id = 0 AND `type` = ?", issueID, commentID, doerID, content)
|
||||||
return DeleteReaction(ctx, &ReactionOptions{
|
_, err := sess.Delete(&Reaction{})
|
||||||
Type: content,
|
return err
|
||||||
DoerID: doerID,
|
|
||||||
IssueID: issueID,
|
|
||||||
CommentID: -1,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteCommentReaction deletes a reaction on comment.
|
func DeleteCommentAllReactions(ctx context.Context, commentID int64) error {
|
||||||
func DeleteCommentReaction(ctx context.Context, doerID, issueID, commentID int64, content string) error {
|
sess := db.GetEngine(ctx).Where("comment_id = ?", commentID)
|
||||||
return DeleteReaction(ctx, &ReactionOptions{
|
_, err := sess.Delete(&Reaction{})
|
||||||
Type: content,
|
return err
|
||||||
DoerID: doerID,
|
}
|
||||||
IssueID: issueID,
|
|
||||||
CommentID: commentID,
|
func DeleteUserReactions(ctx context.Context, userID int64) error {
|
||||||
})
|
sess := db.GetEngine(ctx).Where("user_id = ?", userID)
|
||||||
|
_, err := sess.Delete(&Reaction{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteReleaseReaction(ctx context.Context, doerID, releaseID int64, content string) error {
|
||||||
|
sess := db.GetEngine(ctx).Where("release_id = ? AND user_id = ? AND `type` = ?", releaseID, doerID, content)
|
||||||
|
_, err := sess.Delete(&Reaction{})
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReactionList represents list of reactions
|
// ReactionList represents list of reactions
|
||||||
@@ -302,23 +284,15 @@ func (list ReactionList) getUserIDs() []int64 {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func valuesUser(m map[int64]*user_model.User) []*user_model.User {
|
|
||||||
values := make([]*user_model.User, 0, len(m))
|
|
||||||
for _, v := range m {
|
|
||||||
values = append(values, v)
|
|
||||||
}
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
// newMigrationOriginalUser creates and returns a fake user for external user
|
// newMigrationOriginalUser creates and returns a fake user for external user
|
||||||
func newMigrationOriginalUser(name string) *user_model.User {
|
func newMigrationOriginalUser(name string) *user_model.User {
|
||||||
return &user_model.User{ID: 0, Name: name, LowerName: strings.ToLower(name)}
|
return &user_model.User{ID: 0, Name: name, LowerName: strings.ToLower(name)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadUsers loads reactions' all users
|
// LoadUsers loads reactions' all users
|
||||||
func (list ReactionList) LoadUsers(ctx context.Context, repo *repo_model.Repository) ([]*user_model.User, error) {
|
func (list ReactionList) LoadUsers(ctx context.Context, repo *repo_model.Repository) (map[int64]*user_model.User, error) {
|
||||||
if len(list) == 0 {
|
if len(list) == 0 {
|
||||||
return nil, nil
|
return map[int64]*user_model.User{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
userIDs := list.getUserIDs()
|
userIDs := list.getUserIDs()
|
||||||
@@ -339,7 +313,7 @@ func (list ReactionList) LoadUsers(ctx context.Context, repo *repo_model.Reposit
|
|||||||
reaction.User = user_model.NewGhostUser()
|
reaction.User = user_model.NewGhostUser()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return valuesUser(userMaps), nil
|
return userMaps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFirstUsers returns first reacted user display names separated by comma
|
// GetFirstUsers returns first reacted user display names separated by comma
|
||||||
@@ -350,7 +324,7 @@ func (list ReactionList) GetFirstUsers() string {
|
|||||||
if buffer.Len() > 0 {
|
if buffer.Len() > 0 {
|
||||||
buffer.WriteString(", ")
|
buffer.WriteString(", ")
|
||||||
}
|
}
|
||||||
buffer.WriteString(reaction.User.Name)
|
buffer.WriteString(reaction.User.DisplayNameWithFullName())
|
||||||
if rem--; rem == 0 {
|
if rem--; rem == 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -428,9 +428,6 @@ func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error
|
|||||||
|
|
||||||
// GetCurrentReview returns the current pending review of reviewer for given issue
|
// GetCurrentReview returns the current pending review of reviewer for given issue
|
||||||
func GetCurrentReview(ctx context.Context, reviewer *user_model.User, issue *Issue) (*Review, error) {
|
func GetCurrentReview(ctx context.Context, reviewer *user_model.User, issue *Issue) (*Review, error) {
|
||||||
if reviewer == nil {
|
|
||||||
return nil, nil //nolint:nilnil // return nil when reviewer is nil
|
|
||||||
}
|
|
||||||
reviews, err := FindReviews(ctx, FindReviewOptions{
|
reviews, err := FindReviews(ctx, FindReviewOptions{
|
||||||
Types: []ReviewType{ReviewTypePending},
|
Types: []ReviewType{ReviewTypePending},
|
||||||
IssueID: issue.ID,
|
IssueID: issue.ID,
|
||||||
@@ -442,9 +439,10 @@ func GetCurrentReview(ctx context.Context, reviewer *user_model.User, issue *Iss
|
|||||||
if len(reviews) == 0 {
|
if len(reviews) == 0 {
|
||||||
return nil, ErrReviewNotExist{}
|
return nil, ErrReviewNotExist{}
|
||||||
}
|
}
|
||||||
reviews[0].Reviewer = reviewer
|
pendingReview := reviews[0]
|
||||||
reviews[0].Issue = issue
|
pendingReview.Reviewer = reviewer
|
||||||
return reviews[0], nil
|
pendingReview.Issue = issue
|
||||||
|
return pendingReview, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReviewExists returns whether a review exists for a particular line of code in the PR
|
// ReviewExists returns whether a review exists for a particular line of code in the PR
|
||||||
|
|||||||
@@ -39,6 +39,19 @@ const (
|
|||||||
UpdateStyleRebase UpdateStyle = "rebase"
|
UpdateStyleRebase UpdateStyle = "rebase"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SquashMergeMessage is the source of the default squash merge commit message body, values match GitHub's squash_merge_commit_message: https://docs.github.com/en/rest/repos/repos#update-a-repository
|
||||||
|
type SquashMergeMessage string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SquashMergeMessageBlank SquashMergeMessage = "BLANK"
|
||||||
|
SquashMergeMessageCommitMessages SquashMergeMessage = "COMMIT_MESSAGES"
|
||||||
|
SquashMergeMessagePRBody SquashMergeMessage = "PR_BODY"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m SquashMergeMessage) IsValid() bool {
|
||||||
|
return m == SquashMergeMessageBlank || m == SquashMergeMessageCommitMessages || m == SquashMergeMessagePRBody
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateDefaultBranch updates the default branch
|
// UpdateDefaultBranch updates the default branch
|
||||||
func UpdateDefaultBranch(ctx context.Context, repo *Repository) error {
|
func UpdateDefaultBranch(ctx context.Context, repo *Repository) error {
|
||||||
_, err := db.GetEngine(ctx).ID(repo.ID).Cols("default_branch").Update(repo)
|
_, err := db.GetEngine(ctx).ID(repo.ID).Cols("default_branch").Update(repo)
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ type PullRequestsConfig struct {
|
|||||||
DefaultUpdateStyle UpdateStyle
|
DefaultUpdateStyle UpdateStyle
|
||||||
DefaultDeleteBranchAfterMerge bool
|
DefaultDeleteBranchAfterMerge bool
|
||||||
DefaultMergeStyle MergeStyle
|
DefaultMergeStyle MergeStyle
|
||||||
|
DefaultSquashMergeMessage SquashMergeMessage
|
||||||
DefaultAllowMaintainerEdit bool
|
DefaultAllowMaintainerEdit bool
|
||||||
DefaultTargetBranch string
|
DefaultTargetBranch string
|
||||||
}
|
}
|
||||||
@@ -150,6 +151,7 @@ func DefaultPullRequestsConfig() *PullRequestsConfig {
|
|||||||
cfg.DefaultDeleteBranchAfterMerge = setting.Repository.PullRequest.DefaultDeleteBranchAfterMerge
|
cfg.DefaultDeleteBranchAfterMerge = setting.Repository.PullRequest.DefaultDeleteBranchAfterMerge
|
||||||
cfg.DefaultMergeStyle = MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle)
|
cfg.DefaultMergeStyle = MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle)
|
||||||
cfg.DefaultMergeStyle = util.IfZero(cfg.DefaultMergeStyle, MergeStyleMerge)
|
cfg.DefaultMergeStyle = util.IfZero(cfg.DefaultMergeStyle, MergeStyleMerge)
|
||||||
|
cfg.DefaultSquashMergeMessage = SquashMergeMessage(setting.Repository.PullRequest.DefaultSquashMergeMessage)
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,9 @@ func IsWatchingRepo(ctx context.Context, userID, repoID int64) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func watchRepoByMode(ctx context.Context, watch *Watch, mode WatchMode) (err error) {
|
func watchRepoByMode(ctx context.Context, watch *Watch, mode WatchMode) (err error) {
|
||||||
|
if watch.UserID <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if watch.Mode == mode {
|
if watch.Mode == mode {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func TestGetWatchers(t *testing.T) {
|
|||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
watches, err := repo_model.GetWatchers(t.Context(), repo.ID)
|
watches, err := repo_model.GetWatchers(t.Context(), repo.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
// One watchers are inactive, thus minus 1
|
// One watcher is inactive, thus minus 1
|
||||||
assert.Len(t, watches, repo.NumWatches-1)
|
assert.Len(t, watches, repo.NumWatches-1)
|
||||||
for _, watch := range watches {
|
for _, watch := range watches {
|
||||||
assert.Equal(t, repo.ID, watch.RepoID)
|
assert.Equal(t, repo.ID, watch.RepoID)
|
||||||
@@ -90,24 +90,30 @@ func TestWatchIfAuto(t *testing.T) {
|
|||||||
|
|
||||||
setting.Service.AutoWatchOnChanges = true
|
setting.Service.AutoWatchOnChanges = true
|
||||||
|
|
||||||
// Must not add watch
|
// Must not add watch (user 8 has "don't watch" set)
|
||||||
assert.NoError(t, repo_model.WatchIfAuto(t.Context(), 8, 1, true))
|
assert.NoError(t, repo_model.WatchIfAuto(t.Context(), 8, 1, true))
|
||||||
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
|
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, watchers, prevCount)
|
assert.Len(t, watchers, prevCount)
|
||||||
|
|
||||||
// Should not add watch
|
// Should not add watch (read operation)
|
||||||
assert.NoError(t, repo_model.WatchIfAuto(t.Context(), 12, 1, false))
|
assert.NoError(t, repo_model.WatchIfAuto(t.Context(), 12, 1, false))
|
||||||
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
|
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, watchers, prevCount)
|
assert.Len(t, watchers, prevCount)
|
||||||
|
|
||||||
// Should add watch
|
// Should add watch (write operation)
|
||||||
assert.NoError(t, repo_model.WatchIfAuto(t.Context(), 12, 1, true))
|
assert.NoError(t, repo_model.WatchIfAuto(t.Context(), 12, 1, true))
|
||||||
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
|
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, watchers, prevCount+1)
|
assert.Len(t, watchers, prevCount+1)
|
||||||
|
|
||||||
|
// Should not add watch (system user should not be able to watch)
|
||||||
|
assert.NoError(t, repo_model.WatchIfAuto(t.Context(), user_model.ActionsUserID, 1, true))
|
||||||
|
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, watchers, prevCount+1)
|
||||||
|
|
||||||
// Should remove watch, inhibit from adding auto
|
// Should remove watch, inhibit from adding auto
|
||||||
assert.NoError(t, repo_model.WatchRepoAuto(t.Context(), user12, repo, false))
|
assert.NoError(t, repo_model.WatchRepoAuto(t.Context(), user12, repo, false))
|
||||||
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
|
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
|
||||||
|
|||||||
@@ -465,6 +465,7 @@ func (u *User) EmailTo() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: DefaultShowFullName causes messy logic, there are already too many methods to display a user's "display name", need to refactor them
|
// TODO: DefaultShowFullName causes messy logic, there are already too many methods to display a user's "display name", need to refactor them
|
||||||
|
// Search this TODO to see more details
|
||||||
// * user.Name / user.FullName: directly used in templates
|
// * user.Name / user.FullName: directly used in templates
|
||||||
// * user.DisplayName(): always show FullName if it's not empty, otherwise show Name
|
// * user.DisplayName(): always show FullName if it's not empty, otherwise show Name
|
||||||
// * user.GetDisplayName(): show FullName if it's not empty and DefaultShowFullName is set, otherwise show Name
|
// * user.GetDisplayName(): show FullName if it's not empty and DefaultShowFullName is set, otherwise show Name
|
||||||
@@ -491,6 +492,14 @@ func (u *User) GetDisplayName() string {
|
|||||||
return u.Name
|
return u.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DisplayNameWithFullName returns the username, full name is included if it is not empty
|
||||||
|
func (u *User) DisplayNameWithFullName() string {
|
||||||
|
if fullName := strings.TrimSpace(u.FullName); fullName != "" {
|
||||||
|
return fmt.Sprintf("%s (%s)", u.Name, fullName)
|
||||||
|
}
|
||||||
|
return u.Name
|
||||||
|
}
|
||||||
|
|
||||||
// ShortName ellipses username to length (still used by many templates), it calls GetDisplayName and respects DEFAULT_SHOW_FULL_NAME
|
// ShortName ellipses username to length (still used by many templates), it calls GetDisplayName and respects DEFAULT_SHOW_FULL_NAME
|
||||||
func (u *User) ShortName(length int) string {
|
func (u *User) ShortName(length int) string {
|
||||||
return util.EllipsisDisplayString(u.GetDisplayName(), length)
|
return util.EllipsisDisplayString(u.GetDisplayName(), length)
|
||||||
|
|||||||
@@ -295,18 +295,13 @@ func TestNewGitSig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDisplayName(t *testing.T) {
|
func TestDisplayName(t *testing.T) {
|
||||||
users := make([]*user_model.User, 0, 20)
|
u := &user_model.User{Name: "name"}
|
||||||
err := db.GetEngine(t.Context()).Find(&users)
|
assert.Equal(t, "name", u.DisplayName())
|
||||||
assert.NoError(t, err)
|
assert.Equal(t, "name", u.DisplayNameWithFullName())
|
||||||
|
|
||||||
for _, user := range users {
|
u = &user_model.User{Name: "name", FullName: "Full Name"}
|
||||||
displayName := user.DisplayName()
|
assert.Equal(t, "Full Name", u.DisplayName())
|
||||||
assert.Equal(t, strings.TrimSpace(displayName), displayName)
|
assert.Equal(t, "name (Full Name)", u.DisplayNameWithFullName())
|
||||||
if len(strings.TrimSpace(user.FullName)) == 0 {
|
|
||||||
assert.Equal(t, user.Name, displayName)
|
|
||||||
}
|
|
||||||
assert.NotEmpty(t, strings.TrimSpace(displayName))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateUserEmailAlreadyUsed(t *testing.T) {
|
func TestCreateUserEmailAlreadyUsed(t *testing.T) {
|
||||||
|
|||||||
+11
-2
@@ -7,19 +7,28 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gitea.dev/modules/git/gitcmd"
|
"gitea.dev/modules/git/gitcmd"
|
||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
func LineBlame(ctx context.Context, repo RepositoryFacade, revision, file string, line uint) (string, error) {
|
func LineBlameCommitID(ctx context.Context, repo RepositoryFacade, revision, file string, line uint) (string, error) {
|
||||||
stdout, _, err := gitcmd.NewCommand("blame").WithRepo(repo).
|
stdout, _, err := gitcmd.NewCommand("blame").WithRepo(repo).
|
||||||
AddOptionFormat("-L %d,%d", line, line).
|
AddOptionFormat("-L %d,%d", line, line).
|
||||||
AddOptionValues("-p", revision).
|
AddOptionValues("-p", revision).
|
||||||
AddDashesAndList(file).
|
AddDashesAndList(file).
|
||||||
RunStdString(ctx)
|
RunStdString(ctx)
|
||||||
return stdout, err
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
commitID, _, _ := strings.Cut(stdout, " ")
|
||||||
|
if !IsStringValidObjectID(nil, commitID) {
|
||||||
|
return "", fmt.Errorf("LineBlameCommitID: invalid commit ID %q", commitID)
|
||||||
|
}
|
||||||
|
return commitID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BlamePart represents block of blame - continuous lines with one sha
|
// BlamePart represents block of blame - continuous lines with one sha
|
||||||
|
|||||||
@@ -11,6 +11,18 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestLineBlameCommitID(t *testing.T) {
|
||||||
|
storage := mockRepository("repo5_pulls")
|
||||||
|
const revision = "f32b0a9dfd09a60f616f29158f772cedd89942d2"
|
||||||
|
commitID, err := LineBlameCommitID(t.Context(), storage, revision, "README.md", 1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "72866af952e98d02a73003501836074b286a78f6", commitID)
|
||||||
|
|
||||||
|
commitID, err = LineBlameCommitID(t.Context(), storage, revision, "missing.txt", 1)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Empty(t, commitID)
|
||||||
|
}
|
||||||
|
|
||||||
func TestReadingBlameOutput(t *testing.T) {
|
func TestReadingBlameOutput(t *testing.T) {
|
||||||
setting.AppDataPath = t.TempDir()
|
setting.AppDataPath = t.TempDir()
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
|
|||||||
+6
-14
@@ -46,21 +46,13 @@ func GetDivergingCommits(ctx context.Context, repo RepositoryFacade, baseBranch,
|
|||||||
// GetCommitIDsBetweenReverse returns the last commit IDs between two commits in reverse order (from old to new) with limit.
|
// GetCommitIDsBetweenReverse returns the last commit IDs between two commits in reverse order (from old to new) with limit.
|
||||||
// If the result exceeds the limit, the old commits IDs will be ignored
|
// If the result exceeds the limit, the old commits IDs will be ignored
|
||||||
func GetCommitIDsBetweenReverse(ctx context.Context, repo RepositoryFacade, startRef, endRef, notRef string, limit int) ([]string, error) {
|
func GetCommitIDsBetweenReverse(ctx context.Context, repo RepositoryFacade, startRef, endRef, notRef string, limit int) ([]string, error) {
|
||||||
genCmd := func(reversions ...string) *gitcmd.Command {
|
cmd := gitcmd.NewCommand("rev-list", "--reverse").
|
||||||
cmd := gitcmd.NewCommand("rev-list", "--reverse").
|
AddArguments("-n").AddDynamicArguments(strconv.Itoa(limit)).
|
||||||
AddArguments("-n").AddDynamicArguments(strconv.Itoa(limit)).
|
AddDynamicArguments(startRef + ".." + endRef)
|
||||||
AddDynamicArguments(reversions...)
|
if notRef != "" { // --not should be kept as the last parameter of git command, otherwise the result will be wrong
|
||||||
if notRef != "" { // --not should be kept as the last parameter of git command, otherwise the result will be wrong
|
cmd.AddOptionValues("--not", notRef)
|
||||||
cmd.AddOptionValues("--not", notRef)
|
|
||||||
}
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
stdout, _, err := genCmd(startRef + ".." + endRef).WithRepo(repo).RunStdString(ctx)
|
|
||||||
if gitcmd.IsStderr(err, gitcmd.StderrNoMergeBase) {
|
|
||||||
// if the start and end are not related (no merge base), just get all commits pushed by "end ref"
|
|
||||||
// previously it would return the results of git rev-list before last so let's try that...
|
|
||||||
stdout, _, err = genCmd(endRef).WithRepo(repo).RunStdString(ctx)
|
|
||||||
}
|
}
|
||||||
|
stdout, _, err := cmd.WithRepo(repo).RunStdString(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-17
@@ -5,6 +5,7 @@ package git
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -200,22 +201,18 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
|
|||||||
}
|
}
|
||||||
if old {
|
if old {
|
||||||
begin, _ = strconv.ParseInt(groups["beginOld"], 10, 64)
|
begin, _ = strconv.ParseInt(groups["beginOld"], 10, 64)
|
||||||
end, _ = strconv.ParseInt(groups["endOld"], 10, 64)
|
end, _ = strconv.ParseInt(cmp.Or(groups["endOld"], "1"), 10, 64)
|
||||||
// init otherLine with begin of opposite side
|
// init otherLine with begin of opposite side
|
||||||
otherLine, _ = strconv.ParseInt(groups["beginNew"], 10, 64)
|
otherLine, _ = strconv.ParseInt(groups["beginNew"], 10, 64)
|
||||||
} else {
|
} else {
|
||||||
begin, _ = strconv.ParseInt(groups["beginNew"], 10, 64)
|
begin, _ = strconv.ParseInt(groups["beginNew"], 10, 64)
|
||||||
if groups["endNew"] != "" {
|
end, _ = strconv.ParseInt(cmp.Or(groups["endNew"], "1"), 10, 64)
|
||||||
end, _ = strconv.ParseInt(groups["endNew"], 10, 64)
|
|
||||||
} else {
|
|
||||||
end = 0
|
|
||||||
}
|
|
||||||
// init otherLine with begin of opposite side
|
// init otherLine with begin of opposite side
|
||||||
otherLine, _ = strconv.ParseInt(groups["beginOld"], 10, 64)
|
otherLine, _ = strconv.ParseInt(groups["beginOld"], 10, 64)
|
||||||
}
|
}
|
||||||
end += begin // end is for real only the number of lines in hunk
|
end += begin // end is for real only the number of lines in hunk
|
||||||
// lof is between begin and end
|
// lof is between begin and end
|
||||||
if begin <= line && end >= line {
|
if begin <= line && line < end {
|
||||||
hunk = append(hunk, lof)
|
hunk = append(hunk, lof)
|
||||||
currentLine = begin
|
currentLine = begin
|
||||||
continue
|
continue
|
||||||
@@ -265,27 +262,26 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
|
|||||||
oldBegin = otherLine
|
oldBegin = otherLine
|
||||||
newBegin = currentLine
|
newBegin = currentLine
|
||||||
}
|
}
|
||||||
// headers + hunk header
|
|
||||||
newHunk := make([]string, headerLines)
|
|
||||||
// transfer existing headers
|
|
||||||
copy(newHunk, hunk[:headerLines])
|
|
||||||
// transfer last n lines
|
|
||||||
newHunk = append(newHunk, hunk[len(hunk)-numbersOfLine-1:]...)
|
|
||||||
// calculate newBegin, ... by counting lines
|
// calculate newBegin, ... by counting lines
|
||||||
for i := len(hunk) - 1; i >= len(hunk)-numbersOfLine; i-- {
|
start := len(hunk)
|
||||||
switch hunk[i][0] {
|
for count := 0; count < numbersOfLine && start > headerLines+1; {
|
||||||
|
start--
|
||||||
|
switch hunk[start][0] {
|
||||||
case '+':
|
case '+':
|
||||||
newBegin--
|
newBegin--
|
||||||
newNumOfLines++
|
newNumOfLines++
|
||||||
case '-':
|
case '-':
|
||||||
oldBegin--
|
oldBegin--
|
||||||
oldNumOfLines++
|
oldNumOfLines++
|
||||||
|
case '\\':
|
||||||
|
continue
|
||||||
default:
|
default:
|
||||||
oldBegin--
|
oldBegin--
|
||||||
newBegin--
|
newBegin--
|
||||||
newNumOfLines++
|
newNumOfLines++
|
||||||
oldNumOfLines++
|
oldNumOfLines++
|
||||||
}
|
}
|
||||||
|
count++
|
||||||
}
|
}
|
||||||
|
|
||||||
// "git diff" outputs "@@ -1 +1,3 @@" for "OLD" => "A\nB\nC"
|
// "git diff" outputs "@@ -1 +1,3 @@" for "OLD" => "A\nB\nC"
|
||||||
@@ -294,9 +290,9 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
|
|||||||
// For example: "L1\nL2" => "A\nB", then the patch shows "L2" as line 1 on the left (deleted part)
|
// For example: "L1\nL2" => "A\nB", then the patch shows "L2" as line 1 on the left (deleted part)
|
||||||
|
|
||||||
// construct the new hunk header
|
// construct the new hunk header
|
||||||
newHunk[headerLines] = fmt.Sprintf("@@ -%d,%d +%d,%d @@",
|
hunk[headerLines] = fmt.Sprintf("@@ -%d,%d +%d,%d @@",
|
||||||
oldBegin, oldNumOfLines, newBegin, newNumOfLines)
|
oldBegin, oldNumOfLines, newBegin, newNumOfLines)
|
||||||
return strings.Join(newHunk, "\n"), nil
|
return strings.Join(append(hunk[:headerLines+1], hunk[start:]...), "\n"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAffectedFiles returns the affected files between two commits
|
// GetAffectedFiles returns the affected files between two commits
|
||||||
|
|||||||
@@ -120,10 +120,18 @@ func TestCutDiffAroundLine(t *testing.T) {
|
|||||||
assert.Empty(t, emptyResult)
|
assert.Empty(t, emptyResult)
|
||||||
|
|
||||||
// Line is out of scope
|
// Line is out of scope
|
||||||
emptyResult, err = CutDiffAroundLine(strings.NewReader(exampleDiff), 434, false, 0)
|
emptyResult, err = CutDiffAroundLine(strings.NewReader(exampleDiff), 7, false, 3)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Empty(t, emptyResult)
|
assert.Empty(t, emptyResult)
|
||||||
|
|
||||||
|
noNewlineDiff := "diff --git a/a b/a\n--- a/a\n+++ b/a\n@@ -1 +1 @@\n-a\n\\ No newline at end of file\n+a"
|
||||||
|
result, err = CutDiffAroundLine(strings.NewReader(noNewlineDiff), 1, false, 2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "diff --git a/a b/a\n--- a/a\n+++ b/a\n@@ -1,1 +1,1 @@\n-a\n\\ No newline at end of file\n+a", result)
|
||||||
|
result, err = CutDiffAroundLine(strings.NewReader(noNewlineDiff), 1, true, 2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "diff --git a/a b/a\n--- a/a\n+++ b/a\n@@ -1 +1 @@\n-a", result)
|
||||||
|
|
||||||
// Handle minus diffs properly
|
// Handle minus diffs properly
|
||||||
minusDiff, err := CutDiffAroundLine(strings.NewReader(breakingDiff), 2, false, 4)
|
minusDiff, err := CutDiffAroundLine(strings.NewReader(breakingDiff), 2, false, 4)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|||||||
+7
-3
@@ -68,12 +68,12 @@ func DefaultFeatures() *Features {
|
|||||||
func loadGitVersionFeatures() (*Features, error) {
|
func loadGitVersionFeatures() (*Features, error) {
|
||||||
stdout, _, runErr := gitcmd.NewCommand("version").RunStdString(context.Background())
|
stdout, _, runErr := gitcmd.NewCommand("version").RunStdString(context.Background())
|
||||||
if runErr != nil {
|
if runErr != nil {
|
||||||
return nil, runErr
|
return nil, fmt.Errorf(`failed to run command "git version": %w`, runErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
ver, err := parseGitVersionLine(strings.TrimSpace(stdout))
|
ver, err := parseGitVersionLine(strings.TrimSpace(stdout))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf(`failed to parse output of command "git version": %w`, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
features := &Features{gitVersion: ver}
|
features := &Features{gitVersion: ver}
|
||||||
@@ -104,7 +104,11 @@ func parseGitVersionLine(s string) (*version.Version, error) {
|
|||||||
if len(versionFields) > 3 {
|
if len(versionFields) > 3 {
|
||||||
versionFields = versionFields[:3]
|
versionFields = versionFields[:3]
|
||||||
}
|
}
|
||||||
return version.NewVersion(strings.Join(versionFields, "."))
|
ver, err := version.NewVersion(strings.Join(versionFields, "."))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid git version: %q: %w", s, err)
|
||||||
|
}
|
||||||
|
return ver, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkGitVersionCompatibility(gitVer *version.Version) error {
|
func checkGitVersionCompatibility(gitVer *version.Version) error {
|
||||||
|
|||||||
@@ -93,15 +93,12 @@ const (
|
|||||||
StderrPathSpec StderrPrefix = "fatal: pathspec"
|
StderrPathSpec StderrPrefix = "fatal: pathspec"
|
||||||
StderrBadRevision StderrPrefix = "fatal: bad revision"
|
StderrBadRevision StderrPrefix = "fatal: bad revision"
|
||||||
StderrNoSuchPath StderrPrefix = "fatal: no such path"
|
StderrNoSuchPath StderrPrefix = "fatal: no such path"
|
||||||
|
StderrBadObject StderrPrefix = "fatal: bad object "
|
||||||
StderrNoSuchRemote StderrPrefix = "error: no such remote"
|
|
||||||
|
|
||||||
StderrAuthenticationFailed StderrPrefix = "fatal: Authentication failed for"
|
StderrAuthenticationFailed StderrPrefix = "fatal: Authentication failed for"
|
||||||
StderrCouldNotReadUsername StderrPrefix = "fatal: could not read Username"
|
StderrCouldNotReadUsername StderrPrefix = "fatal: could not read Username"
|
||||||
|
|
||||||
StderrUnknownRevisionOrPath StderrRegexp = "^fatal: .*: unknown revision or path not in the working tree"
|
StderrFileNoEnoughLines StderrRegexp = `^fatal: file .* has only \d+ lines?`
|
||||||
StderrNoMergeBase StderrRegexp = "^fatal: .*: no merge base"
|
|
||||||
StderrFileNoEnoughLines StderrRegexp = `^fatal: file .* has only \d+ lines?`
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func matchStderrCheck(stderr string, checkIntf StderrCheck) (match bool) {
|
func matchStderrCheck(stderr string, checkIntf StderrCheck) (match bool) {
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ func TestIsStderr(t *testing.T) {
|
|||||||
check StderrCheck
|
check StderrCheck
|
||||||
stderr string
|
stderr string
|
||||||
}{
|
}{
|
||||||
{StderrUnknownRevisionOrPath, "fatal: ambiguous argument 'origin': unknown revision or path not in the working tree...."},
|
|
||||||
{StderrNoMergeBase, "fatal: origin/main..HEAD: no merge base...."},
|
|
||||||
{StderrFileNoEnoughLines, "fatal: file foo/bar has only 1 line"},
|
{StderrFileNoEnoughLines, "fatal: file foo/bar has only 1 line"},
|
||||||
{StderrAuthenticationFailed, "Cloning into 'repo'...\r\nremote: Invalid username or token.\r\nfatal: Authentication failed for 'https://host/repo.git/'\r\n"},
|
{StderrAuthenticationFailed, "Cloning into 'repo'...\r\nremote: Invalid username or token.\r\nfatal: Authentication failed for 'https://host/repo.git/'\r\n"},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,9 +64,9 @@ func (err *ErrInvalidCloneAddr) Unwrap() error {
|
|||||||
return util.ErrInvalidArgument
|
return util.ErrInvalidArgument
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsRemoteNotExistError checks the prefix of the error message to see whether a remote does not exist.
|
// IsRemoteNotExistError checks whether a "git remote" subcommand failed because the remote does not exist
|
||||||
func IsRemoteNotExistError(err error) bool {
|
func IsRemoteNotExistError(err error) bool {
|
||||||
return gitcmd.IsStderr(err, gitcmd.StderrNoSuchRemote)
|
return gitcmd.IsErrorExitCode(err, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseRemoteAddr checks if given remote address is valid,
|
// ParseRemoteAddr checks if given remote address is valid,
|
||||||
|
|||||||
@@ -317,28 +317,19 @@ func (repo *Repository) CommitsByFileAndRange(ctx context.Context, opts CommitsB
|
|||||||
// CommitsBetween returns a list that contains commits between [after, before). After is the first item in the slice.
|
// CommitsBetween returns a list that contains commits between [after, before). After is the first item in the slice.
|
||||||
// If "before" and "after" are not related, it returns the all commits for the "after" commit.
|
// If "before" and "after" are not related, it returns the all commits for the "after" commit.
|
||||||
func (repo *Repository) CommitsBetween(ctx context.Context, afterRef, beforeRef RefName, limit int, optSkip ...int) ([]*Commit, error) {
|
func (repo *Repository) CommitsBetween(ctx context.Context, afterRef, beforeRef RefName, limit int, optSkip ...int) ([]*Commit, error) {
|
||||||
gitCmd := func() *gitcmd.Command {
|
cmd := gitcmd.NewCommand("rev-list").WithRepo(repo)
|
||||||
cmd := gitcmd.NewCommand("rev-list").WithRepo(repo)
|
if limit >= 0 {
|
||||||
if limit >= 0 {
|
cmd.AddOptionValues("--max-count", strconv.Itoa(limit))
|
||||||
cmd.AddOptionValues("--max-count", strconv.Itoa(limit))
|
}
|
||||||
}
|
if len(optSkip) > 0 {
|
||||||
if len(optSkip) > 0 {
|
cmd.AddOptionValues("--skip", strconv.Itoa(optSkip[0]))
|
||||||
cmd.AddOptionValues("--skip", strconv.Itoa(optSkip[0]))
|
|
||||||
}
|
|
||||||
return cmd
|
|
||||||
}
|
}
|
||||||
var stdout []byte
|
|
||||||
var err error
|
|
||||||
if beforeRef == "" {
|
if beforeRef == "" {
|
||||||
stdout, _, err = gitCmd().AddDynamicArguments(afterRef.String()).RunStdBytes(ctx)
|
cmd.AddDynamicArguments(afterRef.String())
|
||||||
} else {
|
} else {
|
||||||
stdout, _, err = gitCmd().AddDynamicArguments(beforeRef.String() + ".." + afterRef.String()).RunStdBytes(ctx)
|
cmd.AddDynamicArguments(beforeRef.String() + ".." + afterRef.String())
|
||||||
if gitcmd.IsStderr(err, gitcmd.StderrNoMergeBase) {
|
|
||||||
// future versions of git >= 2.28 are likely to return an error if before and last have become unrelated.
|
|
||||||
// if the beforeRef and afterRef are not related (no merge base), just get all commits pushed by afterRef
|
|
||||||
stdout, _, err = gitCmd().AddDynamicArguments(afterRef.String()).RunStdBytes(ctx)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
stdout, _, err := cmd.RunStdBytes(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,11 +46,6 @@ func (repo *Repository) GetDiffNumChangedFiles(ctx context.Context, base, head s
|
|||||||
WithRepo(repo).
|
WithRepo(repo).
|
||||||
WithStdoutCopy(w).
|
WithStdoutCopy(w).
|
||||||
RunWithStderr(ctx); err != nil {
|
RunWithStderr(ctx); err != nil {
|
||||||
if gitcmd.IsStderr(err, gitcmd.StderrNoMergeBase) {
|
|
||||||
// git >= 2.28 now returns an error if base and head have become unrelated.
|
|
||||||
// it doesn't make sense to count the changed files in this case because UI won't display such diff
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
return w.numLines, nil
|
return w.numLines, nil
|
||||||
|
|||||||
+10
-24
@@ -83,7 +83,7 @@ type Tree struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListEntries returns all entries of current tree.
|
// ListEntries returns all entries of current tree.
|
||||||
func (t *Tree) ListEntries(ctx context.Context, gitRepo *Repository) (Entries, error) {
|
func (t *Tree) ListEntries(_ctx context.Context, gitRepo *Repository) (Entries, error) {
|
||||||
if t.entriesParsed {
|
if t.entriesParsed {
|
||||||
return t.entries, nil
|
return t.entries, nil
|
||||||
}
|
}
|
||||||
@@ -109,36 +109,22 @@ func (t *Tree) ListEntries(ctx context.Context, gitRepo *Repository) (Entries, e
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if info.Type == "tree" {
|
if info.Type != "tree" { // resolve annotated tags to their tree, a blob yields ErrNotExist
|
||||||
t.entries, err = catBatchParseTreeEntries(t.ID.Type(), t, rd, info.Size)
|
if err := DiscardFull(rd, info.Size+1); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
info, rd, err = batch.QueryContent(t.ID.String() + "^{tree}")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
t.entriesParsed = true
|
|
||||||
return t.entries, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not a tree just use ls-tree instead
|
t.entries, err = catBatchParseTreeEntries(t.ID.Type(), t, rd, info.Size)
|
||||||
if err := DiscardFull(rd, info.Size+1); err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
t.entriesParsed = true
|
||||||
stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-l").AddDynamicArguments(t.ID.String()).WithRepo(gitRepo).RunStdBytes(ctx)
|
return t.entries, nil
|
||||||
if runErr != nil {
|
|
||||||
if gitcmd.IsStderr(runErr, gitcmd.StderrNotValidObjectName) || gitcmd.IsStderr(runErr, gitcmd.StderrNotTreeObject) {
|
|
||||||
return nil, ErrNotExist{
|
|
||||||
ID: t.ID.String(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, runErr
|
|
||||||
}
|
|
||||||
|
|
||||||
t.entries, err = parseTreeEntries(stdout, t)
|
|
||||||
if err == nil {
|
|
||||||
t.entriesParsed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return t.entries, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// listEntriesRecursive returns all entries of current tree recursively including all subtrees
|
// listEntriesRecursive returns all entries of current tree recursively including all subtrees
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSubTree_Issue29101(t *testing.T) {
|
func TestSubTree_Issue29101(t *testing.T) {
|
||||||
@@ -26,6 +27,24 @@ func TestSubTree_Issue29101(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTreeListEntriesResolvesTagAndRejectsBlob(t *testing.T) {
|
||||||
|
repo, err := OpenRepositoryLocal(t.Context(), filepath.Join(testReposDir, "repo1_bare"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer repo.Close()
|
||||||
|
|
||||||
|
entries, err := newTree(MustIDFromString("3ad28a9149a2864384548f3d17ed7f38014c9e8a")).ListEntries(t.Context(), repo)
|
||||||
|
require.NoError(t, err)
|
||||||
|
names := make([]string, 0, len(entries))
|
||||||
|
for _, entry := range entries {
|
||||||
|
names = append(names, entry.Name())
|
||||||
|
}
|
||||||
|
assert.Equal(t, []string{"file1.txt", "file2.txt", "foo"}, names)
|
||||||
|
assert.True(t, entries[2].IsDir())
|
||||||
|
|
||||||
|
_, err = newTree(MustIDFromString("e2129701f1a4d54dc44f03c93bca0a2aec7c5449")).ListEntries(t.Context(), repo)
|
||||||
|
assert.True(t, IsErrNotExist(err))
|
||||||
|
}
|
||||||
|
|
||||||
func Test_GetTreePathLatestCommit(t *testing.T) {
|
func Test_GetTreePathLatestCommit(t *testing.T) {
|
||||||
repo, err := OpenRepositoryLocal(t.Context(), filepath.Join(testReposDir, "repo6_blame"))
|
repo, err := OpenRepositoryLocal(t.Context(), filepath.Join(testReposDir, "repo6_blame"))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"io"
|
"io"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"gitea.dev/modules/packages"
|
"gitea.dev/modules/packages"
|
||||||
@@ -24,6 +26,8 @@ var (
|
|||||||
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
|
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
|
||||||
// ErrInvalidVersion indicates an invalid version in the metadata.gz file
|
// ErrInvalidVersion indicates an invalid version in the metadata.gz file
|
||||||
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
|
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
|
||||||
|
// ErrInvalidMetadata indicates metadata that would corrupt the generated index
|
||||||
|
ErrInvalidMetadata = util.NewInvalidArgumentErrorf("package metadata is invalid")
|
||||||
)
|
)
|
||||||
|
|
||||||
var globalVars = sync.OnceValue(func() (ret struct {
|
var globalVars = sync.OnceValue(func() (ret struct {
|
||||||
@@ -221,6 +225,19 @@ func parseMetadataFile(r io.Reader) (*Package, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
values := []string{m.Platform}
|
||||||
|
reqs := slices.Concat(m.RequiredRubyVersion, m.RequiredRubygemsVersion)
|
||||||
|
for _, dep := range slices.Concat(m.RuntimeDependencies, m.DevelopmentDependencies) {
|
||||||
|
values = append(values, dep.Name)
|
||||||
|
reqs = append(reqs, dep.Version...)
|
||||||
|
}
|
||||||
|
for _, req := range reqs {
|
||||||
|
values = append(values, req.Restriction, req.Version)
|
||||||
|
}
|
||||||
|
if slices.ContainsFunc(values, func(s string) bool { return strings.ContainsAny(s, "\r\n") }) {
|
||||||
|
return nil, ErrInvalidMetadata // the compact index is line-based, a line break would forge entries
|
||||||
|
}
|
||||||
|
|
||||||
return &Package{
|
return &Package{
|
||||||
Name: spec.Name,
|
Name: spec.Name,
|
||||||
Version: spec.Version.Version,
|
Version: spec.Version.Version,
|
||||||
|
|||||||
@@ -43,6 +43,19 @@ version:
|
|||||||
assert.Nil(t, rp)
|
assert.Nil(t, rp)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("LineBreakInIndexedMetadata", func(t *testing.T) {
|
||||||
|
for _, field := range []string{
|
||||||
|
`platform: "ruby\nx"`,
|
||||||
|
`dependencies: [{name: "d\nx"}]`,
|
||||||
|
`dependencies: [{name: d, requirement: {requirements: [[">=", {version: "1\nx"}]]}}]`,
|
||||||
|
`required_ruby_version: {requirements: [[">=", {version: "1\nx"}]]}`,
|
||||||
|
} {
|
||||||
|
rp, err := parseMetadataFile(test.CompressGzip("name: g\nversion:\n version: 1\n" + field + "\n"))
|
||||||
|
assert.ErrorIs(t, err, ErrInvalidMetadata, field)
|
||||||
|
assert.Nil(t, rp)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseMetadataFile(t *testing.T) {
|
func TestParseMetadataFile(t *testing.T) {
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gitea.dev/modules/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type deprecatedKey struct {
|
||||||
|
oldSection, oldKey, newSection, newKey string
|
||||||
|
convert func(old ConfigKey) string
|
||||||
|
}
|
||||||
|
|
||||||
|
func invertBool(old ConfigKey) string {
|
||||||
|
return strconv.FormatBool(!old.MustBool())
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep rows, users upgrading from older versions still need them
|
||||||
|
var deprecatedKeys = []deprecatedKey{
|
||||||
|
{"server", "LFS_CONTENT_PATH", "lfs", "PATH", nil},
|
||||||
|
{"server", "ENABLE_LETSENCRYPT", "server", "ENABLE_ACME", nil},
|
||||||
|
{"server", "LETSENCRYPT_ACCEPTTOS", "server", "ACME_ACCEPTTOS", nil},
|
||||||
|
{"server", "LETSENCRYPT_DIRECTORY", "server", "ACME_DIRECTORY", nil},
|
||||||
|
{"server", "LETSENCRYPT_EMAIL", "server", "ACME_EMAIL", nil},
|
||||||
|
{"mailer", "MAILER_TYPE", "mailer", "PROTOCOL", func(old ConfigKey) string { return old.In("", []string{"sendmail"}) }},
|
||||||
|
{"mailer", "IS_TLS_ENABLED", "mailer", "PROTOCOL", func(old ConfigKey) string { return util.Iif(old.MustBool(), "smtps", "smtp+starttls") }},
|
||||||
|
{"mailer", "DISABLE_HELO", "mailer", "ENABLE_HELO", invertBool},
|
||||||
|
{"mailer", "SKIP_VERIFY", "mailer", "FORCE_TRUST_SERVER_CERT", nil},
|
||||||
|
{"mailer", "USE_CERTIFICATE", "mailer", "USE_CLIENT_CERT", nil},
|
||||||
|
{"mailer", "CERT_FILE", "mailer", "CLIENT_CERT_FILE", nil},
|
||||||
|
{"mailer", "KEY_FILE", "mailer", "CLIENT_KEY_FILE", nil},
|
||||||
|
{"mailer", "ENABLE_HTML_ALTERNATIVE", "mailer", "SEND_AS_PLAIN_TEXT", invertBool},
|
||||||
|
{"repository", "DISABLE_MIRRORS", "mirror", "DISABLE_NEW_PULL", nil},
|
||||||
|
{"git.reflog", "ENABLED", "git.config", "core.logAllRefUpdates", func(old ConfigKey) string { return old.In("true", []string{"true", "false"}) }},
|
||||||
|
{"git.reflog", "EXPIRATION", "git.config", "gc.reflogExpire", func(old ConfigKey) string { return old.String() + ".days" }},
|
||||||
|
{"oauth2", "ENABLE", "oauth2", "ENABLED", nil},
|
||||||
|
{"cors", "X_FRAME_OPTIONS", "security", "X_FRAME_OPTIONS", nil},
|
||||||
|
{"repository.pull-request", "POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES", "repository.pull-request", "DEFAULT_SQUASH_MERGE_MESSAGE", func(old ConfigKey) string { return util.Iif(old.MustBool(), "COMMIT_MESSAGES", "PR_BODY") }},
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyDeprecatedKeys copies each deprecated key of the table to its replacement unless the replacement is present
|
||||||
|
func applyDeprecatedKeys(rootCfg ConfigProvider) {
|
||||||
|
for _, deprecated := range deprecatedKeys {
|
||||||
|
oldSec, _ := rootCfg.GetSection(deprecated.oldSection)
|
||||||
|
if oldSec == nil || !oldSec.HasKey(deprecated.oldKey) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
deprecatedSetting(rootCfg, deprecated.oldSection, deprecated.oldKey, deprecated.newSection, deprecated.newKey)
|
||||||
|
if newSec, _ := rootCfg.GetSection(deprecated.newSection); newSec != nil && newSec.HasKey(deprecated.newKey) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
oldKey := oldSec.Key(deprecated.oldKey)
|
||||||
|
value := oldKey.String()
|
||||||
|
if deprecated.convert != nil {
|
||||||
|
value = deprecated.convert(oldKey)
|
||||||
|
}
|
||||||
|
if value != "" {
|
||||||
|
_, _ = rootCfg.Section(deprecated.newSection).NewKey(deprecated.newKey, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -392,9 +392,9 @@ func LogStartupProblem(skip int, level log.Level, format string, args ...any) {
|
|||||||
StartupProblems = append(StartupProblems, msg)
|
StartupProblems = append(StartupProblems, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deprecatedSetting(rootCfg ConfigProvider, oldSection, oldKey, newSection, newKey, version string) {
|
func deprecatedSetting(rootCfg ConfigProvider, oldSection, oldKey, newSection, newKey string) {
|
||||||
if rootCfg.Section(oldSection).HasKey(oldKey) {
|
if rootCfg.Section(oldSection).HasKey(oldKey) {
|
||||||
LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` present, please use `[%s].%s` instead because this fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version)
|
LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` present, please use `[%s].%s` instead", oldSection, oldKey, newSection, newKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,16 +88,6 @@ func loadGitFrom(rootCfg ConfigProvider) {
|
|||||||
GitConfig.SetOption("diff.algorithm", "histogram")
|
GitConfig.SetOption("diff.algorithm", "histogram")
|
||||||
GitConfig.SetOption("core.logAllRefUpdates", "true")
|
GitConfig.SetOption("core.logAllRefUpdates", "true")
|
||||||
|
|
||||||
secGitReflog := rootCfg.Section("git.reflog")
|
|
||||||
if secGitReflog.HasKey("ENABLED") {
|
|
||||||
deprecatedSetting(rootCfg, "git.reflog", "ENABLED", "git.config", "core.logAllRefUpdates", "1.21")
|
|
||||||
GitConfig.SetOption("core.logAllRefUpdates", secGitReflog.Key("ENABLED").In("true", []string{"true", "false"}))
|
|
||||||
}
|
|
||||||
if secGitReflog.HasKey("EXPIRATION") {
|
|
||||||
deprecatedSetting(rootCfg, "git.reflog", "EXPIRATION", "git.config", "gc.reflogExpire", "1.21")
|
|
||||||
GitConfig.SetOption("gc.reflogExpire", secGitReflog.Key("EXPIRATION").String()+".days")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, key := range secGitConfig.Keys() {
|
for _, key := range secGitConfig.Keys() {
|
||||||
GitConfig.SetOption(key.Name(), key.String())
|
GitConfig.SetOption(key.Name(), key.String())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ ENABLED = false
|
|||||||
EXPIRATION = 123
|
EXPIRATION = 123
|
||||||
`)
|
`)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
applyDeprecatedKeys(cfg)
|
||||||
loadGitFrom(cfg)
|
loadGitFrom(cfg)
|
||||||
|
|
||||||
assert.Equal(t, "false", GitConfig.GetOption("core.logAllRefUpdates"))
|
assert.Equal(t, "false", GitConfig.GetOption("core.logAllRefUpdates"))
|
||||||
|
|||||||
@@ -40,18 +40,6 @@ func loadLFSFrom(rootCfg ConfigProvider) error {
|
|||||||
|
|
||||||
lfsSec, _ := rootCfg.GetSection("lfs")
|
lfsSec, _ := rootCfg.GetSection("lfs")
|
||||||
|
|
||||||
// Specifically default PATH to LFS_CONTENT_PATH
|
|
||||||
// DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version
|
|
||||||
// if these are removed, the warning will not be shown
|
|
||||||
deprecatedSetting(rootCfg, "server", "LFS_CONTENT_PATH", "lfs", "PATH", "v1.19.0")
|
|
||||||
|
|
||||||
if val := sec.Key("LFS_CONTENT_PATH").String(); val != "" {
|
|
||||||
if lfsSec == nil {
|
|
||||||
lfsSec = rootCfg.Section("lfs")
|
|
||||||
}
|
|
||||||
lfsSec.Key("PATH").MustString(val) // FIXME: INI-MUST-SIDE-EFFECT
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
LFS.Storage, err = getStorage(rootCfg, "lfs", "", lfsSec)
|
LFS.Storage, err = getStorage(rootCfg, "lfs", "", lfsSec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ PATH = path_used
|
|||||||
`
|
`
|
||||||
cfg, err = NewConfigProviderFromData(iniStr)
|
cfg, err = NewConfigProviderFromData(iniStr)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
applyDeprecatedKeys(cfg)
|
||||||
assert.NoError(t, loadLFSFrom(cfg))
|
assert.NoError(t, loadLFSFrom(cfg))
|
||||||
|
|
||||||
assert.EqualValues(t, "local", LFS.Storage.Type)
|
assert.EqualValues(t, "local", LFS.Storage.Type)
|
||||||
@@ -40,6 +41,7 @@ LFS_CONTENT_PATH = deprecatedpath
|
|||||||
`
|
`
|
||||||
cfg, err = NewConfigProviderFromData(iniStr)
|
cfg, err = NewConfigProviderFromData(iniStr)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
applyDeprecatedKeys(cfg)
|
||||||
assert.NoError(t, loadLFSFrom(cfg))
|
assert.NoError(t, loadLFSFrom(cfg))
|
||||||
|
|
||||||
assert.EqualValues(t, "local", LFS.Storage.Type)
|
assert.EqualValues(t, "local", LFS.Storage.Type)
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ func prepareLoggerConfig(rootCfg ConfigProvider) {
|
|||||||
sec.Key("logger.default.MODE").MustString(",") // FIXME: INI-MUST-SIDE-EFFECT
|
sec.Key("logger.default.MODE").MustString(",") // FIXME: INI-MUST-SIDE-EFFECT
|
||||||
}
|
}
|
||||||
|
|
||||||
deprecatedSetting(rootCfg, "log", "ACCESS", "log", "logger.access.MODE", "1.21")
|
deprecatedSetting(rootCfg, "log", "ACCESS", "log", "logger.access.MODE")
|
||||||
deprecatedSetting(rootCfg, "log", "ENABLE_ACCESS_LOG", "log", "logger.access.MODE", "1.21")
|
deprecatedSetting(rootCfg, "log", "ENABLE_ACCESS_LOG", "log", "logger.access.MODE")
|
||||||
if val := sec.Key("ACCESS").String(); val != "" {
|
if val := sec.Key("ACCESS").String(); val != "" {
|
||||||
sec.Key("logger.access.MODE").MustString(val) // FIXME: INI-MUST-SIDE-EFFECT
|
sec.Key("logger.access.MODE").MustString(val) // FIXME: INI-MUST-SIDE-EFFECT
|
||||||
}
|
}
|
||||||
@@ -68,8 +68,8 @@ func prepareLoggerConfig(rootCfg ConfigProvider) {
|
|||||||
sec.Key("logger.access.MODE").SetValue("")
|
sec.Key("logger.access.MODE").SetValue("")
|
||||||
}
|
}
|
||||||
|
|
||||||
deprecatedSetting(rootCfg, "log", "ROUTER", "log", "logger.router.MODE", "1.21")
|
deprecatedSetting(rootCfg, "log", "ROUTER", "log", "logger.router.MODE")
|
||||||
deprecatedSetting(rootCfg, "log", "DISABLE_ROUTER_LOG", "log", "logger.router.MODE", "1.21")
|
deprecatedSetting(rootCfg, "log", "DISABLE_ROUTER_LOG", "log", "logger.router.MODE")
|
||||||
if val := sec.Key("ROUTER").String(); val != "" {
|
if val := sec.Key("ROUTER").String(); val != "" {
|
||||||
sec.Key("logger.router.MODE").MustString(val) // FIXME: INI-MUST-SIDE-EFFECT
|
sec.Key("logger.router.MODE").MustString(val) // FIXME: INI-MUST-SIDE-EFFECT
|
||||||
}
|
}
|
||||||
@@ -81,8 +81,8 @@ func prepareLoggerConfig(rootCfg ConfigProvider) {
|
|||||||
sec.Key("logger.router.MODE").SetValue("")
|
sec.Key("logger.router.MODE").SetValue("")
|
||||||
}
|
}
|
||||||
|
|
||||||
deprecatedSetting(rootCfg, "log", "XORM", "log", "logger.xorm.MODE", "1.21")
|
deprecatedSetting(rootCfg, "log", "XORM", "log", "logger.xorm.MODE")
|
||||||
deprecatedSetting(rootCfg, "log", "ENABLE_XORM_LOG", "log", "logger.xorm.MODE", "1.21")
|
deprecatedSetting(rootCfg, "log", "ENABLE_XORM_LOG", "log", "logger.xorm.MODE")
|
||||||
if val := sec.Key("XORM").String(); val != "" {
|
if val := sec.Key("XORM").String(); val != "" {
|
||||||
sec.Key("logger.xorm.MODE").MustString(val) // FIXME: INI-MUST-SIDE-EFFECT
|
sec.Key("logger.xorm.MODE").MustString(val) // FIXME: INI-MUST-SIDE-EFFECT
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,14 +76,7 @@ func loadMailerFrom(rootCfg ConfigProvider) {
|
|||||||
// Handle Deprecations and map on to new configuration
|
// Handle Deprecations and map on to new configuration
|
||||||
// DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version
|
// DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version
|
||||||
// if these are removed, the warning will not be shown
|
// if these are removed, the warning will not be shown
|
||||||
deprecatedSetting(rootCfg, "mailer", "MAILER_TYPE", "mailer", "PROTOCOL", "v1.19.0")
|
deprecatedSetting(rootCfg, "mailer", "HOST", "mailer", "SMTP_ADDR")
|
||||||
if sec.HasKey("MAILER_TYPE") && !sec.HasKey("PROTOCOL") {
|
|
||||||
if sec.Key("MAILER_TYPE").String() == "sendmail" {
|
|
||||||
sec.Key("PROTOCOL").MustString("sendmail") // FIXME: INI-MUST-SIDE-EFFECT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deprecatedSetting(rootCfg, "mailer", "HOST", "mailer", "SMTP_ADDR", "v1.19.0")
|
|
||||||
if sec.HasKey("HOST") && !sec.HasKey("SMTP_ADDR") {
|
if sec.HasKey("HOST") && !sec.HasKey("SMTP_ADDR") {
|
||||||
givenHost := sec.Key("HOST").String()
|
givenHost := sec.Key("HOST").String()
|
||||||
addr, port, err := net.SplitHostPort(givenHost)
|
addr, port, err := net.SplitHostPort(givenHost)
|
||||||
@@ -99,47 +92,8 @@ func loadMailerFrom(rootCfg ConfigProvider) {
|
|||||||
sec.Key("SMTP_PORT").MustString(port) // FIXME: INI-MUST-SIDE-EFFECT
|
sec.Key("SMTP_PORT").MustString(port) // FIXME: INI-MUST-SIDE-EFFECT
|
||||||
}
|
}
|
||||||
|
|
||||||
deprecatedSetting(rootCfg, "mailer", "IS_TLS_ENABLED", "mailer", "PROTOCOL", "v1.19.0")
|
|
||||||
if sec.HasKey("IS_TLS_ENABLED") && !sec.HasKey("PROTOCOL") {
|
|
||||||
if sec.Key("IS_TLS_ENABLED").MustBool() {
|
|
||||||
sec.Key("PROTOCOL").MustString("smtps") // FIXME: INI-MUST-SIDE-EFFECT
|
|
||||||
} else {
|
|
||||||
sec.Key("PROTOCOL").MustString("smtp+starttls") // FIXME: INI-MUST-SIDE-EFFECT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deprecatedSetting(rootCfg, "mailer", "DISABLE_HELO", "mailer", "ENABLE_HELO", "v1.19.0")
|
|
||||||
if sec.HasKey("DISABLE_HELO") && !sec.HasKey("ENABLE_HELO") {
|
|
||||||
sec.Key("ENABLE_HELO").MustBool(!sec.Key("DISABLE_HELO").MustBool()) // FIXME: INI-MUST-SIDE-EFFECT
|
|
||||||
}
|
|
||||||
|
|
||||||
deprecatedSetting(rootCfg, "mailer", "SKIP_VERIFY", "mailer", "FORCE_TRUST_SERVER_CERT", "v1.19.0")
|
|
||||||
if sec.HasKey("SKIP_VERIFY") && !sec.HasKey("FORCE_TRUST_SERVER_CERT") {
|
|
||||||
sec.Key("FORCE_TRUST_SERVER_CERT").MustBool(sec.Key("SKIP_VERIFY").MustBool()) // FIXME: INI-MUST-SIDE-EFFECT
|
|
||||||
}
|
|
||||||
|
|
||||||
deprecatedSetting(rootCfg, "mailer", "USE_CERTIFICATE", "mailer", "USE_CLIENT_CERT", "v1.19.0")
|
|
||||||
if sec.HasKey("USE_CERTIFICATE") && !sec.HasKey("USE_CLIENT_CERT") {
|
|
||||||
sec.Key("USE_CLIENT_CERT").MustBool(sec.Key("USE_CERTIFICATE").MustBool()) // FIXME: INI-MUST-SIDE-EFFECT
|
|
||||||
}
|
|
||||||
|
|
||||||
deprecatedSetting(rootCfg, "mailer", "CERT_FILE", "mailer", "CLIENT_CERT_FILE", "v1.19.0")
|
|
||||||
if sec.HasKey("CERT_FILE") && !sec.HasKey("CLIENT_CERT_FILE") {
|
|
||||||
sec.Key("CERT_FILE").MustString(sec.Key("CERT_FILE").String())
|
|
||||||
}
|
|
||||||
|
|
||||||
deprecatedSetting(rootCfg, "mailer", "KEY_FILE", "mailer", "CLIENT_KEY_FILE", "v1.19.0")
|
|
||||||
if sec.HasKey("KEY_FILE") && !sec.HasKey("CLIENT_KEY_FILE") {
|
|
||||||
sec.Key("KEY_FILE").MustString(sec.Key("KEY_FILE").String())
|
|
||||||
}
|
|
||||||
|
|
||||||
deprecatedSetting(rootCfg, "mailer", "ENABLE_HTML_ALTERNATIVE", "mailer", "SEND_AS_PLAIN_TEXT", "v1.19.0")
|
|
||||||
if sec.HasKey("ENABLE_HTML_ALTERNATIVE") && !sec.HasKey("SEND_AS_PLAIN_TEXT") {
|
|
||||||
sec.Key("SEND_AS_PLAIN_TEXT").MustBool(!sec.Key("ENABLE_HTML_ALTERNATIVE").MustBool(false)) // FIXME: INI-MUST-SIDE-EFFECT
|
|
||||||
}
|
|
||||||
|
|
||||||
if sec.HasKey("PROTOCOL") && sec.Key("PROTOCOL").String() == "smtp+startls" {
|
if sec.HasKey("PROTOCOL") && sec.Key("PROTOCOL").String() == "smtp+startls" {
|
||||||
log.Error("Deprecated fallback `[mailer]` `PROTOCOL = smtp+startls` present. Use `[mailer]` `PROTOCOL = smtp+starttls`` instead. This fallback will be removed in v1.19.0")
|
log.Error("Deprecated fallback `[mailer]` `PROTOCOL = smtp+startls` present. Use `[mailer]` `PROTOCOL = smtp+starttls` instead.")
|
||||||
sec.Key("PROTOCOL").SetValue("smtp+starttls")
|
sec.Key("PROTOCOL").SetValue("smtp+starttls")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ func loadMigrationsFrom(rootCfg ConfigProvider) {
|
|||||||
egressModeSet := sec.HasKey("EGRESS_MODE")
|
egressModeSet := sec.HasKey("EGRESS_MODE")
|
||||||
Migrations.EgressMode = normalizePolicyMode(sec.Key("EGRESS_MODE").String())
|
Migrations.EgressMode = normalizePolicyMode(sec.Key("EGRESS_MODE").String())
|
||||||
|
|
||||||
deprecatedSetting(rootCfg, "migrations", "ALLOWED_DOMAINS", "migrations", "ALLOWED_HOST_LIST", "v28.0.0")
|
deprecatedSetting(rootCfg, "migrations", "ALLOWED_DOMAINS", "migrations", "ALLOWED_HOST_LIST")
|
||||||
deprecatedSetting(rootCfg, "migrations", "BLOCKED_DOMAINS", "migrations", "BLOCKED_HOST_LIST", "v28.0.0")
|
deprecatedSetting(rootCfg, "migrations", "BLOCKED_DOMAINS", "migrations", "BLOCKED_HOST_LIST")
|
||||||
deprecatedSetting(rootCfg, "migrations", "ALLOW_LOCALNETWORKS", "migrations", "ALLOWED_HOST_LIST", "v28.0.0")
|
deprecatedSetting(rootCfg, "migrations", "ALLOW_LOCALNETWORKS", "migrations", "ALLOWED_HOST_LIST")
|
||||||
Migrations.AllowedHostList = ConfigSectionKeyString(sec, "ALLOWED_HOST_LIST")
|
Migrations.AllowedHostList = ConfigSectionKeyString(sec, "ALLOWED_HOST_LIST")
|
||||||
if Migrations.AllowedHostList == "" {
|
if Migrations.AllowedHostList == "" {
|
||||||
var hosts []string
|
var hosts []string
|
||||||
|
|||||||
@@ -25,15 +25,6 @@ var Mirror = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadMirrorFrom(rootCfg ConfigProvider) {
|
func loadMirrorFrom(rootCfg ConfigProvider) {
|
||||||
// Handle old configuration through `[repository]` `DISABLE_MIRRORS`
|
|
||||||
// - please note this was badly named and only disabled the creation of new pull mirrors
|
|
||||||
// DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version
|
|
||||||
// if these are removed, the warning will not be shown
|
|
||||||
deprecatedSetting(rootCfg, "repository", "DISABLE_MIRRORS", "mirror", "ENABLED", "v1.19.0")
|
|
||||||
if ConfigSectionKeyBool(rootCfg.Section("repository"), "DISABLE_MIRRORS") {
|
|
||||||
Mirror.DisableNewPull = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := rootCfg.Section("mirror").MapTo(&Mirror); err != nil {
|
if err := rootCfg.Section("mirror").MapTo(&Mirror); err != nil {
|
||||||
log.Fatal("Failed to map Mirror settings: %v", err)
|
log.Fatal("Failed to map Mirror settings: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,12 +122,6 @@ func loadOAuth2From(rootCfg ConfigProvider) {
|
|||||||
OAuth2.DefaultApplications = nil
|
OAuth2.DefaultApplications = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the rename of ENABLE to ENABLED
|
|
||||||
deprecatedSetting(rootCfg, "oauth2", "ENABLE", "oauth2", "ENABLED", "v1.23.0")
|
|
||||||
if sec.HasKey("ENABLE") && !sec.HasKey("ENABLED") {
|
|
||||||
OAuth2.Enabled = sec.Key("ENABLE").MustBool(OAuth2.Enabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !filepath.IsAbs(OAuth2.JWTSigningPrivateKeyFile) {
|
if !filepath.IsAbs(OAuth2.JWTSigningPrivateKeyFile) {
|
||||||
OAuth2.JWTSigningPrivateKeyFile = filepath.Join(AppDataPath, OAuth2.JWTSigningPrivateKeyFile)
|
OAuth2.JWTSigningPrivateKeyFile = filepath.Join(AppDataPath, OAuth2.JWTSigningPrivateKeyFile)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package setting
|
|||||||
import (
|
import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
@@ -94,7 +95,7 @@ var (
|
|||||||
DefaultMergeMessageAllAuthors bool
|
DefaultMergeMessageAllAuthors bool
|
||||||
DefaultMergeMessageMaxApprovers int
|
DefaultMergeMessageMaxApprovers int
|
||||||
DefaultMergeMessageOfficialApproversOnly bool
|
DefaultMergeMessageOfficialApproversOnly bool
|
||||||
PopulateSquashCommentWithCommitMessages bool
|
DefaultSquashMergeMessage string
|
||||||
AddCoCommitterTrailers bool
|
AddCoCommitterTrailers bool
|
||||||
RetargetChildrenOnMerge bool
|
RetargetChildrenOnMerge bool
|
||||||
DelayCheckForInactiveDays int
|
DelayCheckForInactiveDays int
|
||||||
@@ -188,7 +189,7 @@ var (
|
|||||||
DefaultMergeMessageAllAuthors bool
|
DefaultMergeMessageAllAuthors bool
|
||||||
DefaultMergeMessageMaxApprovers int
|
DefaultMergeMessageMaxApprovers int
|
||||||
DefaultMergeMessageOfficialApproversOnly bool
|
DefaultMergeMessageOfficialApproversOnly bool
|
||||||
PopulateSquashCommentWithCommitMessages bool
|
DefaultSquashMergeMessage string
|
||||||
AddCoCommitterTrailers bool
|
AddCoCommitterTrailers bool
|
||||||
RetargetChildrenOnMerge bool
|
RetargetChildrenOnMerge bool
|
||||||
DelayCheckForInactiveDays int
|
DelayCheckForInactiveDays int
|
||||||
@@ -206,7 +207,7 @@ var (
|
|||||||
DefaultMergeMessageAllAuthors: false,
|
DefaultMergeMessageAllAuthors: false,
|
||||||
DefaultMergeMessageMaxApprovers: 10,
|
DefaultMergeMessageMaxApprovers: 10,
|
||||||
DefaultMergeMessageOfficialApproversOnly: true,
|
DefaultMergeMessageOfficialApproversOnly: true,
|
||||||
PopulateSquashCommentWithCommitMessages: false,
|
DefaultSquashMergeMessage: "PR_BODY",
|
||||||
AddCoCommitterTrailers: true,
|
AddCoCommitterTrailers: true,
|
||||||
RetargetChildrenOnMerge: true,
|
RetargetChildrenOnMerge: true,
|
||||||
DelayCheckForInactiveDays: 7,
|
DelayCheckForInactiveDays: 7,
|
||||||
@@ -339,6 +340,10 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
|
|||||||
log.Fatal("Failed to map Repository.PullRequest settings: %v", err)
|
log.Fatal("Failed to map Repository.PullRequest settings: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !slices.Contains([]string{"BLANK", "COMMIT_MESSAGES", "PR_BODY"}, Repository.PullRequest.DefaultSquashMergeMessage) { // uppercase unlike other config values, to match GitHub's squash_merge_commit_message which the API also uses
|
||||||
|
log.Fatal("Invalid repository.pull-request.DEFAULT_SQUASH_MERGE_MESSAGE: %s", Repository.PullRequest.DefaultSquashMergeMessage)
|
||||||
|
}
|
||||||
|
|
||||||
if !rootCfg.Section("packages").Key("ENABLED").MustBool(Packages.Enabled) {
|
if !rootCfg.Section("packages").Key("ENABLED").MustBool(Packages.Enabled) {
|
||||||
Repository.DisabledRepoUnits = append(Repository.DisabledRepoUnits, "repo.packages")
|
Repository.DisabledRepoUnits = append(Repository.DisabledRepoUnits, "repo.packages")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,10 +180,6 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
|
|||||||
PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false)
|
PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false)
|
||||||
SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20)
|
SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20)
|
||||||
|
|
||||||
deprecatedSetting(rootCfg, "cors", "X_FRAME_OPTIONS", "security", "X_FRAME_OPTIONS", "v1.26.0")
|
|
||||||
if !sec.HasKey("X_FRAME_OPTIONS") {
|
|
||||||
Security.XFrameOptions = rootCfg.Section("cors").Key("X_FRAME_OPTIONS").MustString(Security.XFrameOptions)
|
|
||||||
}
|
|
||||||
if err := sec.MapTo(&Security); err != nil {
|
if err := sec.MapTo(&Security); err != nil {
|
||||||
log.Fatal("Failed to map security settings: %v", err)
|
log.Fatal("Failed to map security settings: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,14 +151,7 @@ func loadServerFrom(rootCfg ConfigProvider) {
|
|||||||
HTTPAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0")
|
HTTPAddr = sec.Key("HTTP_ADDR").MustString("0.0.0.0")
|
||||||
HTTPPort = sec.Key("HTTP_PORT").MustString("3000")
|
HTTPPort = sec.Key("HTTP_PORT").MustString("3000")
|
||||||
|
|
||||||
// DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version
|
EnableAcme = sec.Key("ENABLE_ACME").MustBool(false)
|
||||||
// if these are removed, the warning will not be shown
|
|
||||||
if sec.HasKey("ENABLE_ACME") {
|
|
||||||
EnableAcme = sec.Key("ENABLE_ACME").MustBool(false)
|
|
||||||
} else {
|
|
||||||
deprecatedSetting(rootCfg, "server", "ENABLE_LETSENCRYPT", "server", "ENABLE_ACME", "v1.19.0")
|
|
||||||
EnableAcme = sec.Key("ENABLE_LETSENCRYPT").MustBool(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
protocolCfg := sec.Key("PROTOCOL").String()
|
protocolCfg := sec.Key("PROTOCOL").String()
|
||||||
if protocolCfg != "https" && EnableAcme {
|
if protocolCfg != "https" && EnableAcme {
|
||||||
@@ -175,29 +168,12 @@ func loadServerFrom(rootCfg ConfigProvider) {
|
|||||||
AcmeProfile = sec.Key("ACME_PROFILE").MustString("")
|
AcmeProfile = sec.Key("ACME_PROFILE").MustString("")
|
||||||
AcmeCARoot = sec.Key("ACME_CA_ROOT").MustString("")
|
AcmeCARoot = sec.Key("ACME_CA_ROOT").MustString("")
|
||||||
|
|
||||||
if sec.HasKey("ACME_ACCEPTTOS") {
|
AcmeTOS = sec.Key("ACME_ACCEPTTOS").MustBool(false)
|
||||||
AcmeTOS = sec.Key("ACME_ACCEPTTOS").MustBool(false)
|
|
||||||
} else {
|
|
||||||
deprecatedSetting(rootCfg, "server", "LETSENCRYPT_ACCEPTTOS", "server", "ACME_ACCEPTTOS", "v1.19.0")
|
|
||||||
AcmeTOS = sec.Key("LETSENCRYPT_ACCEPTTOS").MustBool(false)
|
|
||||||
}
|
|
||||||
if !AcmeTOS {
|
if !AcmeTOS {
|
||||||
log.Fatal("ACME TOS is not accepted (ACME_ACCEPTTOS).")
|
log.Fatal("ACME TOS is not accepted (ACME_ACCEPTTOS).")
|
||||||
}
|
}
|
||||||
|
AcmeLiveDirectory = sec.Key("ACME_DIRECTORY").MustString("https")
|
||||||
if sec.HasKey("ACME_DIRECTORY") {
|
AcmeEmail = sec.Key("ACME_EMAIL").MustString("")
|
||||||
AcmeLiveDirectory = sec.Key("ACME_DIRECTORY").MustString("https")
|
|
||||||
} else {
|
|
||||||
deprecatedSetting(rootCfg, "server", "LETSENCRYPT_DIRECTORY", "server", "ACME_DIRECTORY", "v1.19.0")
|
|
||||||
AcmeLiveDirectory = sec.Key("LETSENCRYPT_DIRECTORY").MustString("https")
|
|
||||||
}
|
|
||||||
|
|
||||||
if sec.HasKey("ACME_EMAIL") {
|
|
||||||
AcmeEmail = sec.Key("ACME_EMAIL").MustString("")
|
|
||||||
} else {
|
|
||||||
deprecatedSetting(rootCfg, "server", "LETSENCRYPT_EMAIL", "server", "ACME_EMAIL", "v1.19.0")
|
|
||||||
AcmeEmail = sec.Key("LETSENCRYPT_EMAIL").MustString("")
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
CertFile = sec.Key("CERT_FILE").String()
|
CertFile = sec.Key("CERT_FILE").String()
|
||||||
KeyFile = sec.Key("KEY_FILE").String()
|
KeyFile = sec.Key("KEY_FILE").String()
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ func loadServiceFrom(rootCfg ConfigProvider) {
|
|||||||
Service.RegisterManualConfirm = false
|
Service.RegisterManualConfirm = false
|
||||||
}
|
}
|
||||||
if sec.HasKey("EMAIL_DOMAIN_WHITELIST") {
|
if sec.HasKey("EMAIL_DOMAIN_WHITELIST") {
|
||||||
deprecatedSetting(rootCfg, "service", "EMAIL_DOMAIN_WHITELIST", "service", "EMAIL_DOMAIN_ALLOWLIST", "1.21")
|
deprecatedSetting(rootCfg, "service", "EMAIL_DOMAIN_WHITELIST", "service", "EMAIL_DOMAIN_ALLOWLIST")
|
||||||
}
|
}
|
||||||
Service.EmailDomainAllowList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_WHITELIST", "EMAIL_DOMAIN_ALLOWLIST")
|
Service.EmailDomainAllowList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_WHITELIST", "EMAIL_DOMAIN_ALLOWLIST")
|
||||||
Service.EmailDomainBlockList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_BLOCKLIST")
|
Service.EmailDomainBlockList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_BLOCKLIST")
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ func LoadCommonSettings() {
|
|||||||
func loadCommonSettingsFrom(cfg ConfigProvider) error {
|
func loadCommonSettingsFrom(cfg ConfigProvider) error {
|
||||||
// a lot of logic depends on InstallLock value, so it must be loaded before any other settings
|
// a lot of logic depends on InstallLock value, so it must be loaded before any other settings
|
||||||
InstallLock = HasInstallLock(cfg)
|
InstallLock = HasInstallLock(cfg)
|
||||||
|
applyDeprecatedKeys(cfg)
|
||||||
|
|
||||||
// WARNING: don't change the sequence except you know what you are doing.
|
// WARNING: don't change the sequence except you know what you are doing.
|
||||||
loadRunModeFrom(cfg)
|
loadRunModeFrom(cfg)
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ func loadTaskFrom(rootCfg ConfigProvider) {
|
|||||||
taskSec := rootCfg.Section("task")
|
taskSec := rootCfg.Section("task")
|
||||||
queueTaskSec := rootCfg.Section("queue.task")
|
queueTaskSec := rootCfg.Section("queue.task")
|
||||||
|
|
||||||
deprecatedSetting(rootCfg, "task", "QUEUE_TYPE", "queue.task", "TYPE", "v1.19.0")
|
deprecatedSetting(rootCfg, "task", "QUEUE_TYPE", "queue.task", "TYPE")
|
||||||
deprecatedSetting(rootCfg, "task", "QUEUE_CONN_STR", "queue.task", "CONN_STR", "v1.19.0")
|
deprecatedSetting(rootCfg, "task", "QUEUE_CONN_STR", "queue.task", "CONN_STR")
|
||||||
deprecatedSetting(rootCfg, "task", "QUEUE_LENGTH", "queue.task", "LENGTH", "v1.19.0")
|
deprecatedSetting(rootCfg, "task", "QUEUE_LENGTH", "queue.task", "LENGTH")
|
||||||
|
|
||||||
switch taskSec.Key("QUEUE_TYPE").MustString("channel") {
|
switch taskSec.Key("QUEUE_TYPE").MustString("channel") {
|
||||||
case "channel":
|
case "channel":
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ var UI = struct {
|
|||||||
FileIconTheme string
|
FileIconTheme string
|
||||||
FolderIconTheme string
|
FolderIconTheme string
|
||||||
Reactions []string
|
Reactions []string
|
||||||
|
ReleaseReactions []string
|
||||||
ReactionsLookup container.Set[string] `ini:"-"`
|
ReactionsLookup container.Set[string] `ini:"-"`
|
||||||
|
ReleaseReactionsLookup container.Set[string] `ini:"-"`
|
||||||
CustomEmojis []string
|
CustomEmojis []string
|
||||||
CustomEmojisMap map[string]string `ini:"-"`
|
CustomEmojisMap map[string]string `ini:"-"`
|
||||||
EnabledEmojis []string
|
EnabledEmojis []string
|
||||||
@@ -98,6 +100,7 @@ var UI = struct {
|
|||||||
FileIconTheme: `material`,
|
FileIconTheme: `material`,
|
||||||
FolderIconTheme: `basic`,
|
FolderIconTheme: `basic`,
|
||||||
Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`},
|
Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`},
|
||||||
|
ReleaseReactions: []string{`+1`, `laugh`, `hooray`, `heart`, `rocket`, `eyes`},
|
||||||
CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`},
|
CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`},
|
||||||
CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"},
|
CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"},
|
||||||
ExploreDefaultSort: "recentupdate",
|
ExploreDefaultSort: "recentupdate",
|
||||||
@@ -170,10 +173,9 @@ func loadUIFrom(rootCfg ConfigProvider) {
|
|||||||
// because many private repositories do not have "description/topic", users just want to search by their names.
|
// because many private repositories do not have "description/topic", users just want to search by their names.
|
||||||
UI.OnlyShowRelevantRepos = sec.Key("ONLY_SHOW_RELEVANT_REPOS").MustBool(false)
|
UI.OnlyShowRelevantRepos = sec.Key("ONLY_SHOW_RELEVANT_REPOS").MustBool(false)
|
||||||
|
|
||||||
UI.ReactionsLookup = make(container.Set[string])
|
UI.ReactionsLookup = container.SetOf(UI.Reactions...)
|
||||||
for _, reaction := range UI.Reactions {
|
UI.ReleaseReactionsLookup = container.SetOf(UI.ReleaseReactions...)
|
||||||
UI.ReactionsLookup.Add(reaction)
|
|
||||||
}
|
|
||||||
UI.CustomEmojisMap = make(map[string]string)
|
UI.CustomEmojisMap = make(map[string]string)
|
||||||
for _, emoji := range UI.CustomEmojis {
|
for _, emoji := range UI.CustomEmojis {
|
||||||
UI.CustomEmojisMap[emoji] = ":" + emoji + ":"
|
UI.CustomEmojisMap[emoji] = ":" + emoji + ":"
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func loadWebhookFrom(rootCfg ConfigProvider) {
|
|||||||
Webhook.DeliverTimeout = sec.Key("DELIVER_TIMEOUT").MustInt(5)
|
Webhook.DeliverTimeout = sec.Key("DELIVER_TIMEOUT").MustInt(5)
|
||||||
Webhook.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool()
|
Webhook.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool()
|
||||||
|
|
||||||
deprecatedSetting(rootCfg, "webhook", "ALLOWED_HOST_LIST", "security", "ALLOWED_HOST_LIST", "v28.0.0")
|
deprecatedSetting(rootCfg, "webhook", "ALLOWED_HOST_LIST", "security", "ALLOWED_HOST_LIST")
|
||||||
Webhook.AllowedHostList = sec.Key("ALLOWED_HOST_LIST").MustString(Security.AllowedHostList)
|
Webhook.AllowedHostList = sec.Key("ALLOWED_HOST_LIST").MustString(Security.AllowedHostList)
|
||||||
if Webhook.AllowedHostList != Security.AllowedHostList {
|
if Webhook.AllowedHostList != Security.AllowedHostList {
|
||||||
checkHostList("[webhook] ALLOWED_HOST_LIST", Webhook.AllowedHostList, false)
|
checkHostList("[webhook] ALLOWED_HOST_LIST", Webhook.AllowedHostList, false)
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ type Repository struct {
|
|||||||
DefaultDeleteBranchAfterMerge bool `json:"default_delete_branch_after_merge"`
|
DefaultDeleteBranchAfterMerge bool `json:"default_delete_branch_after_merge"`
|
||||||
DefaultMergeStyle string `json:"default_merge_style"`
|
DefaultMergeStyle string `json:"default_merge_style"`
|
||||||
DefaultUpdateStyle string `json:"default_update_style"`
|
DefaultUpdateStyle string `json:"default_update_style"`
|
||||||
|
SquashMergeCommitMessage string `json:"squash_merge_commit_message"`
|
||||||
DefaultAllowMaintainerEdit bool `json:"default_allow_maintainer_edit"`
|
DefaultAllowMaintainerEdit bool `json:"default_allow_maintainer_edit"`
|
||||||
AvatarURL string `json:"avatar_url"`
|
AvatarURL string `json:"avatar_url"`
|
||||||
Internal bool `json:"internal"`
|
Internal bool `json:"internal"`
|
||||||
@@ -236,6 +237,8 @@ type EditRepoOption struct {
|
|||||||
DefaultMergeStyle *string `json:"default_merge_style,omitempty"`
|
DefaultMergeStyle *string `json:"default_merge_style,omitempty"`
|
||||||
// set to an update style to be used by this repository: "merge" or "rebase".
|
// set to an update style to be used by this repository: "merge" or "rebase".
|
||||||
DefaultUpdateStyle *string `json:"default_update_style,omitempty"`
|
DefaultUpdateStyle *string `json:"default_update_style,omitempty"`
|
||||||
|
// set the default squash merge commit message body: "PR_BODY", "COMMIT_MESSAGES", or "BLANK".
|
||||||
|
SquashMergeCommitMessage *string `json:"squash_merge_commit_message,omitempty"`
|
||||||
// set to `true` to allow edits from maintainers by default
|
// set to `true` to allow edits from maintainers by default
|
||||||
DefaultAllowMaintainerEdit *bool `json:"default_allow_maintainer_edit,omitempty"`
|
DefaultAllowMaintainerEdit *bool `json:"default_allow_maintainer_edit,omitempty"`
|
||||||
// set to `true` to archive this repository.
|
// set to `true` to archive this repository.
|
||||||
|
|||||||
@@ -93,9 +93,6 @@ func newFuncMapWebPage() template.FuncMap {
|
|||||||
"ShowFooterPoweredBy": func() bool {
|
"ShowFooterPoweredBy": func() bool {
|
||||||
return setting.Other.ShowFooterPoweredBy
|
return setting.Other.ShowFooterPoweredBy
|
||||||
},
|
},
|
||||||
"AllowedReactions": func() []string {
|
|
||||||
return setting.UI.Reactions
|
|
||||||
},
|
|
||||||
"MetaAuthor": func() string {
|
"MetaAuthor": func() string {
|
||||||
return setting.UI.Meta.Author
|
return setting.UI.Meta.Author
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2148,6 +2148,10 @@
|
|||||||
"repo.settings.pulls.allow_merge_update": "Enable updating pull request branch by merge",
|
"repo.settings.pulls.allow_merge_update": "Enable updating pull request branch by merge",
|
||||||
"repo.settings.pulls.allow_rebase_update": "Enable updating pull request branch by rebase",
|
"repo.settings.pulls.allow_rebase_update": "Enable updating pull request branch by rebase",
|
||||||
"repo.settings.pulls.default_update_style": "Default branch update style",
|
"repo.settings.pulls.default_update_style": "Default branch update style",
|
||||||
|
"repo.settings.pulls.default_squash_merge_message": "Default squash merge commit message",
|
||||||
|
"repo.settings.pulls.default_squash_merge_message.title": "Pull request title",
|
||||||
|
"repo.settings.pulls.default_squash_merge_message.title_and_commit_details": "Pull request title and commit details",
|
||||||
|
"repo.settings.pulls.default_squash_merge_message.title_and_description": "Pull request title and description",
|
||||||
"repo.settings.pulls.default_target_branch": "Default target branch for new pull requests",
|
"repo.settings.pulls.default_target_branch": "Default target branch for new pull requests",
|
||||||
"repo.settings.pulls.default_target_branch_default": "Default branch (%s)",
|
"repo.settings.pulls.default_target_branch_default": "Default branch (%s)",
|
||||||
"repo.settings.pulls.default_delete_branch_after_merge": "Delete pull request branch after merge by default",
|
"repo.settings.pulls.default_delete_branch_after_merge": "Delete pull request branch after merge by default",
|
||||||
|
|||||||
@@ -602,7 +602,7 @@
|
|||||||
"user.followers": "abonnés",
|
"user.followers": "abonnés",
|
||||||
"user.show_more": "Voir plus",
|
"user.show_more": "Voir plus",
|
||||||
"user.starred": "Dépôts favoris",
|
"user.starred": "Dépôts favoris",
|
||||||
"user.watched": "Dépôts surveillés",
|
"user.watched": "Dépôts observés",
|
||||||
"user.code": "Code",
|
"user.code": "Code",
|
||||||
"user.projects": "Projets",
|
"user.projects": "Projets",
|
||||||
"user.overview": "Vue d'ensemble",
|
"user.overview": "Vue d'ensemble",
|
||||||
@@ -929,6 +929,7 @@
|
|||||||
"settings.delete_account_title": "Supprimer ce compte d’utilisateur",
|
"settings.delete_account_title": "Supprimer ce compte d’utilisateur",
|
||||||
"settings.delete_account_desc": "Êtes-vous sûr de vouloir supprimer définitivement ce compte d’utilisateur ?",
|
"settings.delete_account_desc": "Êtes-vous sûr de vouloir supprimer définitivement ce compte d’utilisateur ?",
|
||||||
"settings.email_notifications.enable": "Notifier par courriel",
|
"settings.email_notifications.enable": "Notifier par courriel",
|
||||||
|
"settings.email_notifications.onmention_and_custom": "Courriel uniquement sur Mentions et Observations personnalisées",
|
||||||
"settings.email_notifications.disable": "Ne pas notifier",
|
"settings.email_notifications.disable": "Ne pas notifier",
|
||||||
"settings.email_notifications.submit": "Définir les préférences de courriel",
|
"settings.email_notifications.submit": "Définir les préférences de courriel",
|
||||||
"settings.email_notifications.andyourown": "Inclure vos propres notifications",
|
"settings.email_notifications.andyourown": "Inclure vos propres notifications",
|
||||||
@@ -1141,10 +1142,10 @@
|
|||||||
"repo.generated_from": "généré depuis",
|
"repo.generated_from": "généré depuis",
|
||||||
"repo.fork_from_self": "Vous ne pouvez pas bifurquer un dépôt que vous possédez.",
|
"repo.fork_from_self": "Vous ne pouvez pas bifurquer un dépôt que vous possédez.",
|
||||||
"repo.fork_guest_user": "Connectez-vous pour bifurquer ce dépôt.",
|
"repo.fork_guest_user": "Connectez-vous pour bifurquer ce dépôt.",
|
||||||
"repo.watch_guest_user": "Connectez-vous pour consulter ce dépôt.",
|
"repo.watch_guest_user": "Connectez-vous pour observer ce dépôt.",
|
||||||
"repo.star_guest_user": "Connectez-vous pour ajouter aux favoris ce dépôt.",
|
"repo.star_guest_user": "Connectez-vous pour ajouter aux favoris ce dépôt.",
|
||||||
"repo.watch": "Suivre",
|
"repo.watch": "Observer",
|
||||||
"repo.watching": "Suivi",
|
"repo.watching": "Observé",
|
||||||
"repo.ignoring": "Ignoré",
|
"repo.ignoring": "Ignoré",
|
||||||
"repo.watch.options.issues": "Tickets",
|
"repo.watch.options.issues": "Tickets",
|
||||||
"repo.watch.options.pull_requests": "Demandes d'ajout",
|
"repo.watch.options.pull_requests": "Demandes d'ajout",
|
||||||
@@ -2511,7 +2512,7 @@
|
|||||||
"repo.settings.visibility.private.text": "Rendre le dépôt privé rendra non seulement le dépôt visible uniquement aux membres autorisés, mais peut également rompre la relation entre lui et ses bifurcations, observateurs, et favoris.",
|
"repo.settings.visibility.private.text": "Rendre le dépôt privé rendra non seulement le dépôt visible uniquement aux membres autorisés, mais peut également rompre la relation entre lui et ses bifurcations, observateurs, et favoris.",
|
||||||
"repo.settings.visibility.private.bullet_title": "<strong>Changer la visibilité en privé :</strong>",
|
"repo.settings.visibility.private.bullet_title": "<strong>Changer la visibilité en privé :</strong>",
|
||||||
"repo.settings.visibility.private.bullet_one": "Rendra le dépôt visible uniquement aux membres autorisés.",
|
"repo.settings.visibility.private.bullet_one": "Rendra le dépôt visible uniquement aux membres autorisés.",
|
||||||
"repo.settings.visibility.private.bullet_two": "Applique la visibilité aux bifurcation et retire les <strong>observateurs</strong> et les <strong>favoris</strong>.",
|
"repo.settings.visibility.private.bullet_two": "Applique la visibilité aux bifurcations et retire les <strong>observateurs</strong> et les <strong>favoris</strong>.",
|
||||||
"repo.settings.visibility.private.stats_stars": "Il y a <strong>%d</strong> favori(s) sur ce dépôt qui pourrai(en)t être perdu(s).",
|
"repo.settings.visibility.private.stats_stars": "Il y a <strong>%d</strong> favori(s) sur ce dépôt qui pourrai(en)t être perdu(s).",
|
||||||
"repo.settings.visibility.private.stats_watchers": "Il y a <strong>%d</strong> observateur(s) sur ce dépôt qui pourrai(en)t être perdu(s).",
|
"repo.settings.visibility.private.stats_watchers": "Il y a <strong>%d</strong> observateur(s) sur ce dépôt qui pourrai(en)t être perdu(s).",
|
||||||
"repo.settings.visibility.private.stats_forks": "Il y a <strong>%d</strong> bifurcation(s) associée(s) à ce dépôt.",
|
"repo.settings.visibility.private.stats_forks": "Il y a <strong>%d</strong> bifurcation(s) associée(s) à ce dépôt.",
|
||||||
@@ -3548,7 +3549,7 @@
|
|||||||
"notification.mark_all_as_read": "Tout marquer comme lu",
|
"notification.mark_all_as_read": "Tout marquer comme lu",
|
||||||
"notification.mark_page_as_read": "Marquer cette page comme lue",
|
"notification.mark_page_as_read": "Marquer cette page comme lue",
|
||||||
"notification.subscriptions": "Abonnements",
|
"notification.subscriptions": "Abonnements",
|
||||||
"notification.watching": "Suivi",
|
"notification.watching": "Observé",
|
||||||
"notification.no_subscriptions": "Pas d'abonnements",
|
"notification.no_subscriptions": "Pas d'abonnements",
|
||||||
"gpg.default_key": "Signé avec la clé par défaut",
|
"gpg.default_key": "Signé avec la clé par défaut",
|
||||||
"gpg.error.extract_sign": "Impossible d'extraire la signature",
|
"gpg.error.extract_sign": "Impossible d'extraire la signature",
|
||||||
|
|||||||
@@ -926,6 +926,7 @@
|
|||||||
"settings.delete_account_title": "Scrios Cuntas Úsáide",
|
"settings.delete_account_title": "Scrios Cuntas Úsáide",
|
||||||
"settings.delete_account_desc": "An bhfuil tú cinnte gur mhaith leat an cuntas úsáideora seo a scriosadh go buan?",
|
"settings.delete_account_desc": "An bhfuil tú cinnte gur mhaith leat an cuntas úsáideora seo a scriosadh go buan?",
|
||||||
"settings.email_notifications.enable": "Cumasaigh Fógraí Ríomhphoist",
|
"settings.email_notifications.enable": "Cumasaigh Fógraí Ríomhphoist",
|
||||||
|
"settings.email_notifications.onmention_and_custom": "Ríomhphost amháin nuair a dhéantar tagairt, agus uaireadóirí saincheaptha",
|
||||||
"settings.email_notifications.disable": "Díchumasaigh Fógraí Ríomhphoist",
|
"settings.email_notifications.disable": "Díchumasaigh Fógraí Ríomhphoist",
|
||||||
"settings.email_notifications.submit": "Socraigh rogha ríomhphoist",
|
"settings.email_notifications.submit": "Socraigh rogha ríomhphoist",
|
||||||
"settings.email_notifications.andyourown": "Agus Do Fógraí Féin",
|
"settings.email_notifications.andyourown": "Agus Do Fógraí Féin",
|
||||||
@@ -3987,6 +3988,8 @@
|
|||||||
"actions.general.token_permissions.maximum.description": "Beidh ceadanna éifeachtacha an phoist gníomhartha teoranta ag na ceadanna uasta.",
|
"actions.general.token_permissions.maximum.description": "Beidh ceadanna éifeachtacha an phoist gníomhartha teoranta ag na ceadanna uasta.",
|
||||||
"actions.general.token_permissions.fork_pr_note": "Mura gcuirtear tús le post trí iarratas tarraingthe ó fhorc, ní rachaidh a cheadanna éifeachtacha thar na ceadanna léite amháin.",
|
"actions.general.token_permissions.fork_pr_note": "Mura gcuirtear tús le post trí iarratas tarraingthe ó fhorc, ní rachaidh a cheadanna éifeachtacha thar na ceadanna léite amháin.",
|
||||||
"actions.general.token_permissions.customize_max_permissions": "Saincheap na ceadanna uasta",
|
"actions.general.token_permissions.customize_max_permissions": "Saincheap na ceadanna uasta",
|
||||||
|
"actions.general.token_permissions.id_token": "Comhartha OpenID Connect",
|
||||||
|
"actions.general.token_permissions.id_token.description": "Ceadaigh do phoist a dhearbhaíonn \"id-token: write\" comharthaí OpenID Connect gearrshaolacha a iarraidh chun fíordheimhniú a dhéanamh i gcoinne seirbhísí seachtracha.",
|
||||||
"actions.general.cross_repo": "Rochtain Tras-Stórtha",
|
"actions.general.cross_repo": "Rochtain Tras-Stórtha",
|
||||||
"actions.general.cross_repo_desc": "Ceadaigh rochtain (léamh amháin) a bheith ag na stórtha uile san úinéir seo ar na stórtha roghnaithe le GITEA_TOKEN agus poist Gníomhartha á reáchtáil.",
|
"actions.general.cross_repo_desc": "Ceadaigh rochtain (léamh amháin) a bheith ag na stórtha uile san úinéir seo ar na stórtha roghnaithe le GITEA_TOKEN agus poist Gníomhartha á reáchtáil.",
|
||||||
"actions.general.cross_repo_selected": "Stórtha roghnaithe",
|
"actions.general.cross_repo_selected": "Stórtha roghnaithe",
|
||||||
|
|||||||
@@ -87,6 +87,7 @@
|
|||||||
"add_all": "すべて追加",
|
"add_all": "すべて追加",
|
||||||
"dismiss": "閉じる",
|
"dismiss": "閉じる",
|
||||||
"remove": "除去",
|
"remove": "除去",
|
||||||
|
"rename": "名前を変更",
|
||||||
"remove_all": "すべて除去",
|
"remove_all": "すべて除去",
|
||||||
"remove_label_str": "アイテム「%s」を削除",
|
"remove_label_str": "アイテム「%s」を削除",
|
||||||
"edit": "編集",
|
"edit": "編集",
|
||||||
@@ -906,6 +907,7 @@
|
|||||||
"settings.webauthn_desc": "セキュリティキーは暗号化キーを内蔵するハードウェア ・ デバイスです。 2要素認証に使用できます。 セキュリティキーは<a rel=\"noreferrer\" target=\"_blank\" href=\"%s\">WebAuthn Authenticator</a>規格をサポートしている必要があります。",
|
"settings.webauthn_desc": "セキュリティキーは暗号化キーを内蔵するハードウェア ・ デバイスです。 2要素認証に使用できます。 セキュリティキーは<a rel=\"noreferrer\" target=\"_blank\" href=\"%s\">WebAuthn Authenticator</a>規格をサポートしている必要があります。",
|
||||||
"settings.webauthn_register_key": "セキュリティキーを追加",
|
"settings.webauthn_register_key": "セキュリティキーを追加",
|
||||||
"settings.webauthn_nickname": "ニックネーム",
|
"settings.webauthn_nickname": "ニックネーム",
|
||||||
|
"settings.webauthn_nickname_been_used": "同じニックネームのシークレットキーが既に存在しています。",
|
||||||
"settings.webauthn_delete_key": "セキュリティキーの登録解除",
|
"settings.webauthn_delete_key": "セキュリティキーの登録解除",
|
||||||
"settings.webauthn_delete_key_desc": "セキュリティキーの登録を解除すると、今後そのセキュリティキーでサインインすることはできなくなります。 続行しますか?",
|
"settings.webauthn_delete_key_desc": "セキュリティキーの登録を解除すると、今後そのセキュリティキーでサインインすることはできなくなります。 続行しますか?",
|
||||||
"settings.webauthn_key_loss_warning": "セキュリティキーを紛失すると、アカウントへのアクセスを失います。",
|
"settings.webauthn_key_loss_warning": "セキュリティキーを紛失すると、アカウントへのアクセスを失います。",
|
||||||
@@ -2296,6 +2298,7 @@
|
|||||||
"repo.settings.event_release_desc": "リポジトリでリリースが作成・更新・削除されたとき。",
|
"repo.settings.event_release_desc": "リポジトリでリリースが作成・更新・削除されたとき。",
|
||||||
"repo.settings.event_push": "プッシュ",
|
"repo.settings.event_push": "プッシュ",
|
||||||
"repo.settings.event_force_push": "強制プッシュ",
|
"repo.settings.event_force_push": "強制プッシュ",
|
||||||
|
"repo.settings.event_branch_delete": "ブランチの抹消",
|
||||||
"repo.settings.event_push_desc": "Gitがリポジトリにプッシュを行ったとき。",
|
"repo.settings.event_push_desc": "Gitがリポジトリにプッシュを行ったとき。",
|
||||||
"repo.settings.event_repository": "リポジトリ",
|
"repo.settings.event_repository": "リポジトリ",
|
||||||
"repo.settings.event_repository_desc": "リポジトリが作成・削除・名前変更されたとき。",
|
"repo.settings.event_repository_desc": "リポジトリが作成・削除・名前変更されたとき。",
|
||||||
@@ -2407,6 +2410,12 @@
|
|||||||
"repo.settings.protect_enable_force_push_all_desc": "プッシュ権限を持つ人は誰でも、このブランチに強制プッシュを行うことができます。",
|
"repo.settings.protect_enable_force_push_all_desc": "プッシュ権限を持つ人は誰でも、このブランチに強制プッシュを行うことができます。",
|
||||||
"repo.settings.protect_enable_force_push_allowlist": "許可リストで強制プッシュを制限",
|
"repo.settings.protect_enable_force_push_allowlist": "許可リストで強制プッシュを制限",
|
||||||
"repo.settings.protect_enable_force_push_allowlist_desc": "許可リストに登録された、プッシュ権限を持つユーザーまたはチームのみが、このブランチに強制プッシュを行うことができます。",
|
"repo.settings.protect_enable_force_push_allowlist_desc": "許可リストに登録された、プッシュ権限を持つユーザーまたはチームのみが、このブランチに強制プッシュを行うことができます。",
|
||||||
|
"repo.settings.protect_disable_deletion": "ブランチの抹消を無効化",
|
||||||
|
"repo.settings.protect_disable_deletion_desc": "このブランチではブランチ抹消は許可されません。",
|
||||||
|
"repo.settings.protect_enable_deletion_all": "ブランチの抹消を有効化",
|
||||||
|
"repo.settings.protect_enable_deletion_all_desc": "プッシュ権限を持つ人は誰でも、このブランチの抹消を行うことができます。",
|
||||||
|
"repo.settings.protect_enable_deletion_allowlist": "許可リストでブランチ抹消を制限",
|
||||||
|
"repo.settings.protect_enable_deletion_allowlist_desc": "許可リストに登録された、プッシュ権限を持つユーザーまたはチームのみが、このブランチの抹消を行うことができます。",
|
||||||
"repo.settings.protect_enable_merge": "マージ有効",
|
"repo.settings.protect_enable_merge": "マージ有効",
|
||||||
"repo.settings.protect_enable_merge_desc": "書き込み権限を持つ人は誰でも、このブランチにプルリクエストをマージすることができます。",
|
"repo.settings.protect_enable_merge_desc": "書き込み権限を持つ人は誰でも、このブランチにプルリクエストをマージすることができます。",
|
||||||
"repo.settings.protect_whitelist_committers": "許可リストでプッシュを制限",
|
"repo.settings.protect_whitelist_committers": "許可リストでプッシュを制限",
|
||||||
@@ -2417,6 +2426,8 @@
|
|||||||
"repo.settings.protect_force_push_allowlist_users": "強制プッシュ許可リストに含むユーザー:",
|
"repo.settings.protect_force_push_allowlist_users": "強制プッシュ許可リストに含むユーザー:",
|
||||||
"repo.settings.protect_force_push_allowlist_teams": "強制プッシュ許可リストに含むチーム:",
|
"repo.settings.protect_force_push_allowlist_teams": "強制プッシュ許可リストに含むチーム:",
|
||||||
"repo.settings.protect_force_push_allowlist_deploy_keys": "強制プッシュ可能なプッシュ権限を持つデプロイキーを許可リストに含める。",
|
"repo.settings.protect_force_push_allowlist_deploy_keys": "強制プッシュ可能なプッシュ権限を持つデプロイキーを許可リストに含める。",
|
||||||
|
"repo.settings.protect_deletion_allowlist_users": "抹消許可リストに含むユーザー:",
|
||||||
|
"repo.settings.protect_deletion_allowlist_teams": "抹消許可リストに含むチーム:",
|
||||||
"repo.settings.protect_merge_whitelist_committers": "マージ許可リストを有効にする",
|
"repo.settings.protect_merge_whitelist_committers": "マージ許可リストを有効にする",
|
||||||
"repo.settings.protect_merge_whitelist_committers_desc": "許可リストに登録したユーザーまたはチームにのみ、このブランチへのプルリクエストのマージを許可します。",
|
"repo.settings.protect_merge_whitelist_committers_desc": "許可リストに登録したユーザーまたはチームにのみ、このブランチへのプルリクエストのマージを許可します。",
|
||||||
"repo.settings.protect_merge_whitelist_users": "マージ許可リストに含むユーザー:",
|
"repo.settings.protect_merge_whitelist_users": "マージ許可リストに含むユーザー:",
|
||||||
|
|||||||
@@ -929,6 +929,7 @@
|
|||||||
"settings.delete_account_title": "Eliminar conta de utilizador",
|
"settings.delete_account_title": "Eliminar conta de utilizador",
|
||||||
"settings.delete_account_desc": "Tem a certeza que quer eliminar permanentemente esta conta de utilizador?",
|
"settings.delete_account_desc": "Tem a certeza que quer eliminar permanentemente esta conta de utilizador?",
|
||||||
"settings.email_notifications.enable": "Habilitar notificações por email",
|
"settings.email_notifications.enable": "Habilitar notificações por email",
|
||||||
|
"settings.email_notifications.onmention_and_custom": "Email somente em vigilâncias de menções e personalizadas",
|
||||||
"settings.email_notifications.disable": "Desabilitar notificações por email",
|
"settings.email_notifications.disable": "Desabilitar notificações por email",
|
||||||
"settings.email_notifications.submit": "Definir preferência do email",
|
"settings.email_notifications.submit": "Definir preferência do email",
|
||||||
"settings.email_notifications.andyourown": "e as suas próprias notificações",
|
"settings.email_notifications.andyourown": "e as suas próprias notificações",
|
||||||
@@ -4000,6 +4001,8 @@
|
|||||||
"actions.general.token_permissions.maximum.description": "As permissões concretas para o trabalho das operações serão limitadas pelas permissões máximas.",
|
"actions.general.token_permissions.maximum.description": "As permissões concretas para o trabalho das operações serão limitadas pelas permissões máximas.",
|
||||||
"actions.general.token_permissions.fork_pr_note": "Se o trabalho for iniciado por um pedido de integração de uma derivação, as suas permissões concretas não irão exceder as permissões apenas de leitura.",
|
"actions.general.token_permissions.fork_pr_note": "Se o trabalho for iniciado por um pedido de integração de uma derivação, as suas permissões concretas não irão exceder as permissões apenas de leitura.",
|
||||||
"actions.general.token_permissions.customize_max_permissions": "Personalizar as permissões máximas",
|
"actions.general.token_permissions.customize_max_permissions": "Personalizar as permissões máximas",
|
||||||
|
"actions.general.token_permissions.id_token": "Código OpenID Connect",
|
||||||
|
"actions.general.token_permissions.id_token.description": "Permitir que trabalhos que declaram \"id-token: write\" solicitem códigos OpenID Connect de curta duração para se autenticarem em serviços externos.",
|
||||||
"actions.general.cross_repo": "Acesso entre repositórios",
|
"actions.general.cross_repo": "Acesso entre repositórios",
|
||||||
"actions.general.cross_repo_desc": "Permitir que os repositórios escolhidos sejam acedidos (somente leitura) por todos os repositórios deste proprietário com GITEA_TOKEN ao executar os trabalhos das operações.",
|
"actions.general.cross_repo_desc": "Permitir que os repositórios escolhidos sejam acedidos (somente leitura) por todos os repositórios deste proprietário com GITEA_TOKEN ao executar os trabalhos das operações.",
|
||||||
"actions.general.cross_repo_selected": "Repositórios escolhidos",
|
"actions.general.cross_repo_selected": "Repositórios escolhidos",
|
||||||
|
|||||||
@@ -929,6 +929,7 @@
|
|||||||
"settings.delete_account_title": "删除当前帐户",
|
"settings.delete_account_title": "删除当前帐户",
|
||||||
"settings.delete_account_desc": "确定要永久删除此用户帐户吗?",
|
"settings.delete_account_desc": "确定要永久删除此用户帐户吗?",
|
||||||
"settings.email_notifications.enable": "启用邮件通知",
|
"settings.email_notifications.enable": "启用邮件通知",
|
||||||
|
"settings.email_notifications.onmention_and_custom": "仅在被提及或发生自定义关注事件时发送邮件",
|
||||||
"settings.email_notifications.disable": "停用邮件通知",
|
"settings.email_notifications.disable": "停用邮件通知",
|
||||||
"settings.email_notifications.submit": "设置邮件通知",
|
"settings.email_notifications.submit": "设置邮件通知",
|
||||||
"settings.email_notifications.andyourown": "同时接收由自己操作触发的通知",
|
"settings.email_notifications.andyourown": "同时接收由自己操作触发的通知",
|
||||||
@@ -2298,6 +2299,7 @@
|
|||||||
"repo.settings.event_release_desc": "发布、更新或删除版本时。",
|
"repo.settings.event_release_desc": "发布、更新或删除版本时。",
|
||||||
"repo.settings.event_push": "推送",
|
"repo.settings.event_push": "推送",
|
||||||
"repo.settings.event_force_push": "强制推送",
|
"repo.settings.event_force_push": "强制推送",
|
||||||
|
"repo.settings.event_branch_delete": "分支删除",
|
||||||
"repo.settings.event_push_desc": "Git 仓库推送",
|
"repo.settings.event_push_desc": "Git 仓库推送",
|
||||||
"repo.settings.event_repository": "仓库",
|
"repo.settings.event_repository": "仓库",
|
||||||
"repo.settings.event_repository_desc": "仓库被创建、删除或重命名。",
|
"repo.settings.event_repository_desc": "仓库被创建、删除或重命名。",
|
||||||
@@ -2409,6 +2411,12 @@
|
|||||||
"repo.settings.protect_enable_force_push_all_desc": "任何拥有推送权限的人都可以对这个分支进行强制推送。",
|
"repo.settings.protect_enable_force_push_all_desc": "任何拥有推送权限的人都可以对这个分支进行强制推送。",
|
||||||
"repo.settings.protect_enable_force_push_allowlist": "强制推送白名单",
|
"repo.settings.protect_enable_force_push_allowlist": "强制推送白名单",
|
||||||
"repo.settings.protect_enable_force_push_allowlist_desc": "只有具有推送权限且列入白名单的用户或团队,才能强制推送到此分支。",
|
"repo.settings.protect_enable_force_push_allowlist_desc": "只有具有推送权限且列入白名单的用户或团队,才能强制推送到此分支。",
|
||||||
|
"repo.settings.protect_disable_deletion": "禁止删除分支",
|
||||||
|
"repo.settings.protect_disable_deletion_desc": "不允许删除此分支。",
|
||||||
|
"repo.settings.protect_enable_deletion_all": "允许删除分支",
|
||||||
|
"repo.settings.protect_enable_deletion_all_desc": "任何拥有推送权限的人都可以删除此分支。",
|
||||||
|
"repo.settings.protect_enable_deletion_allowlist": "分支删除白名单",
|
||||||
|
"repo.settings.protect_enable_deletion_allowlist_desc": "只有具有推送权限且列入白名单的用户或团队,才能删除此分支。",
|
||||||
"repo.settings.protect_enable_merge": "启用合并",
|
"repo.settings.protect_enable_merge": "启用合并",
|
||||||
"repo.settings.protect_enable_merge_desc": "任何具有写入权限的人都可以将合并请求合并到此分支中。",
|
"repo.settings.protect_enable_merge_desc": "任何具有写入权限的人都可以将合并请求合并到此分支中。",
|
||||||
"repo.settings.protect_whitelist_committers": "受白名单限制的推送",
|
"repo.settings.protect_whitelist_committers": "受白名单限制的推送",
|
||||||
@@ -2419,6 +2427,8 @@
|
|||||||
"repo.settings.protect_force_push_allowlist_users": "允许强制推送的白名单用户:",
|
"repo.settings.protect_force_push_allowlist_users": "允许强制推送的白名单用户:",
|
||||||
"repo.settings.protect_force_push_allowlist_teams": "允许强制推送的白名单团队:",
|
"repo.settings.protect_force_push_allowlist_teams": "允许强制推送的白名单团队:",
|
||||||
"repo.settings.protect_force_push_allowlist_deploy_keys": "允许具有推送权限的白名单部署密钥进行强制推送。",
|
"repo.settings.protect_force_push_allowlist_deploy_keys": "允许具有推送权限的白名单部署密钥进行强制推送。",
|
||||||
|
"repo.settings.protect_deletion_allowlist_users": "删除白名单用户:",
|
||||||
|
"repo.settings.protect_deletion_allowlist_teams": "删除白名单团队:",
|
||||||
"repo.settings.protect_merge_whitelist_committers": "启用合并白名单",
|
"repo.settings.protect_merge_whitelist_committers": "启用合并白名单",
|
||||||
"repo.settings.protect_merge_whitelist_committers_desc": "仅允许白名单用户或团队合并合并请求到此分支。",
|
"repo.settings.protect_merge_whitelist_committers_desc": "仅允许白名单用户或团队合并合并请求到此分支。",
|
||||||
"repo.settings.protect_merge_whitelist_users": "合并白名单用户:",
|
"repo.settings.protect_merge_whitelist_users": "合并白名单用户:",
|
||||||
@@ -3991,6 +4001,8 @@
|
|||||||
"actions.general.token_permissions.maximum.description": "工作流任务的实际权限将受到最大权限的限制。",
|
"actions.general.token_permissions.maximum.description": "工作流任务的实际权限将受到最大权限的限制。",
|
||||||
"actions.general.token_permissions.fork_pr_note": "如果任务是由来自派生仓库的合并请求触发的,那么它的实际权限不会超过只读权限。",
|
"actions.general.token_permissions.fork_pr_note": "如果任务是由来自派生仓库的合并请求触发的,那么它的实际权限不会超过只读权限。",
|
||||||
"actions.general.token_permissions.customize_max_permissions": "自定义最大权限",
|
"actions.general.token_permissions.customize_max_permissions": "自定义最大权限",
|
||||||
|
"actions.general.token_permissions.id_token": "OpenID Connect 令牌",
|
||||||
|
"actions.general.token_permissions.id_token.description": "允许声明了「id-token: write」的作业请求短期有效的 OpenID Connect 令牌,以向外部服务进行身份验证。",
|
||||||
"actions.general.cross_repo": "跨仓库访问",
|
"actions.general.cross_repo": "跨仓库访问",
|
||||||
"actions.general.cross_repo_desc": "允许此所有者下的所有仓库在运行工作流任务时,通过 GITEA_TOKEN 以只读方式访问所选仓库。",
|
"actions.general.cross_repo_desc": "允许此所有者下的所有仓库在运行工作流任务时,通过 GITEA_TOKEN 以只读方式访问所选仓库。",
|
||||||
"actions.general.cross_repo_selected": "选择的仓库",
|
"actions.general.cross_repo_selected": "选择的仓库",
|
||||||
|
|||||||
@@ -335,8 +335,8 @@ func ListIssues(ctx *context.APIContext) {
|
|||||||
keyword := ctx.FormTrim("q")
|
keyword := ctx.FormTrim("q")
|
||||||
|
|
||||||
var labelIDs []int64
|
var labelIDs []int64
|
||||||
if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
|
if labelsParam := ctx.FormTrim("labels"); labelsParam != "" {
|
||||||
labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, splitted)
|
labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, strings.Split(labelsParam, ","))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func GetIssueCommentReactions(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
reactions, _, err := issues_model.FindCommentReactions(ctx, comment.IssueID, comment.ID)
|
reactions, _, err := issues_model.FindCommentReactions(ctx, comment.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -981,14 +981,23 @@ func MergePullRequest(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message := strings.TrimSpace(form.MergeTitleField)
|
message := strings.TrimSpace(form.MergeTitleField)
|
||||||
if len(message) == 0 {
|
useDefaultSquashBody := !form.HasMergeMessageField && !ctx.Req.Form.Has("merge_message_field") && repo_model.MergeStyle(form.Do) == repo_model.MergeStyleSquash
|
||||||
message, _, err = pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pr, repo_model.MergeStyle(form.Do))
|
if len(message) == 0 || useDefaultSquashBody {
|
||||||
|
defaultTitle, defaultBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pr, repo_model.MergeStyle(form.Do))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
message = util.IfZero(message, defaultTitle)
|
||||||
|
if useDefaultSquashBody {
|
||||||
|
squashCommitMessages, err := pull_service.GetSquashMergeCommitMessages(ctx, pr)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
form.MergeMessageField = git.CommitMessageMerge(squashCommitMessages, defaultBody)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
form.MergeMessageField = strings.TrimSpace(form.MergeMessageField)
|
form.MergeMessageField = strings.TrimSpace(form.MergeMessageField)
|
||||||
if len(form.MergeMessageField) > 0 {
|
if len(form.MergeMessageField) > 0 {
|
||||||
message += "\n\n" + form.MergeMessageField
|
message += "\n\n" + form.MergeMessageField
|
||||||
|
|||||||
@@ -895,6 +895,10 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
|
|||||||
optional.AssignPtrValue(changed, &config.DefaultAllowMaintainerEdit, opts.DefaultAllowMaintainerEdit)
|
optional.AssignPtrValue(changed, &config.DefaultAllowMaintainerEdit, opts.DefaultAllowMaintainerEdit)
|
||||||
optional.AssignPtrString(changed, &config.DefaultMergeStyle, opts.DefaultMergeStyle)
|
optional.AssignPtrString(changed, &config.DefaultMergeStyle, opts.DefaultMergeStyle)
|
||||||
optional.AssignPtrString(changed, &config.DefaultUpdateStyle, opts.DefaultUpdateStyle)
|
optional.AssignPtrString(changed, &config.DefaultUpdateStyle, opts.DefaultUpdateStyle)
|
||||||
|
optional.AssignPtrString(changed, &config.DefaultSquashMergeMessage, opts.SquashMergeCommitMessage)
|
||||||
|
if opts.SquashMergeCommitMessage != nil && !config.DefaultSquashMergeMessage.IsValid() {
|
||||||
|
return util.NewInvalidArgumentErrorf("invalid squash_merge_commit_message: %s", config.DefaultSquashMergeMessage)
|
||||||
|
}
|
||||||
// only validate update-style fields when the caller is actually changing one of them,
|
// only validate update-style fields when the caller is actually changing one of them,
|
||||||
// so unrelated PATCH calls don't reject historical configs.
|
// so unrelated PATCH calls don't reject historical configs.
|
||||||
if opts.AllowMergeUpdate != nil || opts.AllowRebaseUpdate != nil || opts.DefaultUpdateStyle != nil {
|
if opts.AllowMergeUpdate != nil || opts.AllowRebaseUpdate != nil || opts.DefaultUpdateStyle != nil {
|
||||||
|
|||||||
@@ -669,7 +669,6 @@ func (cpi *comparePageInfoType) prepareCreatePullRequestPage(ctx *context.Contex
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.Data["IsProjectsEnabled"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
|
ctx.Data["IsProjectsEnabled"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
|
||||||
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
|
||||||
upload.AddUploadContext(ctx, "comment")
|
upload.AddUploadContext(ctx, "comment")
|
||||||
|
|
||||||
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWrite(unit.TypePullRequests)
|
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWrite(unit.TypePullRequests)
|
||||||
|
|||||||
+10
-14
@@ -25,6 +25,7 @@ import (
|
|||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
"gitea.dev/modules/markup/markdown"
|
"gitea.dev/modules/markup/markdown"
|
||||||
"gitea.dev/modules/optional"
|
"gitea.dev/modules/optional"
|
||||||
|
"gitea.dev/modules/setting"
|
||||||
api "gitea.dev/modules/structs"
|
api "gitea.dev/modules/structs"
|
||||||
"gitea.dev/modules/templates"
|
"gitea.dev/modules/templates"
|
||||||
"gitea.dev/modules/util"
|
"gitea.dev/modules/util"
|
||||||
@@ -45,7 +46,7 @@ const (
|
|||||||
tplIssueView templates.TplName = "repo/issue/view"
|
tplIssueView templates.TplName = "repo/issue/view"
|
||||||
|
|
||||||
tplPullMergeBox templates.TplName = "repo/issue/view_content/pull_merge_box"
|
tplPullMergeBox templates.TplName = "repo/issue/view_content/pull_merge_box"
|
||||||
tplReactions templates.TplName = "repo/issue/view_content/reactions"
|
tplReactions templates.TplName = "shared/reactions"
|
||||||
|
|
||||||
issueTemplateKey = "IssueTemplate"
|
issueTemplateKey = "IssueTemplate"
|
||||||
issueTemplateTitleKey = "IssueTemplateTitle"
|
issueTemplateTitleKey = "IssueTemplateTitle"
|
||||||
@@ -536,25 +537,20 @@ func ChangeIssueReaction(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(issue.Reactions) == 0 {
|
renderReactions(ctx, fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index), issue.Reactions, setting.UI.Reactions)
|
||||||
ctx.JSON(http.StatusOK, map[string]any{
|
}
|
||||||
"empty": true,
|
|
||||||
"html": "",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
func renderReactions(ctx *context.Context, actionURL string, reactions issues_model.ReactionList, allowedReactions []string) {
|
||||||
html, err := ctx.RenderToHTML(tplReactions, map[string]any{
|
html, err := ctx.RenderToHTML(tplReactions, map[string]any{
|
||||||
"ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index),
|
"ActionURL": actionURL,
|
||||||
"Reactions": issue.Reactions.GroupByType(),
|
"Reactions": reactions.GroupByType(),
|
||||||
|
"AllowedReactions": allowedReactions,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("ChangeIssueReaction.HTMLString", err)
|
ctx.ServerError("RenderToHTML", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.JSON(http.StatusOK, map[string]any{
|
ctx.JSON(http.StatusOK, map[string]any{"html": html})
|
||||||
"html": html,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIssueAttachments returns attachments for the issue
|
// GetIssueAttachments returns attachments for the issue
|
||||||
|
|||||||
@@ -300,25 +300,7 @@ func ChangeCommentReaction(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(comment.Reactions) == 0 {
|
renderReactions(ctx, fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID), comment.Reactions, setting.UI.Reactions)
|
||||||
ctx.JSON(http.StatusOK, map[string]any{
|
|
||||||
"empty": true,
|
|
||||||
"html": "",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
html, err := ctx.RenderToHTML(tplReactions, map[string]any{
|
|
||||||
"ActionURL": fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID),
|
|
||||||
"Reactions": comment.Reactions.GroupByType(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("ChangeCommentReaction.HTMLString", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.JSON(http.StatusOK, map[string]any{
|
|
||||||
"html": html,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCommentAttachments returns attachments for the comment
|
// GetCommentAttachments returns attachments for the comment
|
||||||
|
|||||||
@@ -111,7 +111,6 @@ func NewIssue(ctx *context.Context) {
|
|||||||
|
|
||||||
isProjectsEnabled := ctx.Repo.Permission.CanRead(unit.TypeProjects)
|
isProjectsEnabled := ctx.Repo.Permission.CanRead(unit.TypeProjects)
|
||||||
ctx.Data["IsProjectsEnabled"] = isProjectsEnabled
|
ctx.Data["IsProjectsEnabled"] = isProjectsEnabled
|
||||||
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
|
||||||
upload.AddUploadContext(ctx, "comment")
|
upload.AddUploadContext(ctx, "comment")
|
||||||
|
|
||||||
pageMetaData := retrieveRepoIssueMetaDataWithPrefill(ctx, ctx.Repo.Repository, false)
|
pageMetaData := retrieveRepoIssueMetaDataWithPrefill(ctx, ctx.Repo.Repository, false)
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ func ViewIssue(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.Data["IsProjectsEnabled"] = ctx.Repo.Permission.CanRead(unit.TypeProjects)
|
ctx.Data["IsProjectsEnabled"] = ctx.Repo.Permission.CanRead(unit.TypeProjects)
|
||||||
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
ctx.Data["AllowedIssueReactions"] = setting.UI.Reactions
|
||||||
upload.AddUploadContext(ctx, "comment")
|
upload.AddUploadContext(ctx, "comment")
|
||||||
|
|
||||||
if err := issue.LoadAttributes(ctx); err != nil {
|
if err := issue.LoadAttributes(ctx); err != nil {
|
||||||
|
|||||||
+21
-18
@@ -370,8 +370,7 @@ func (prInfo *pullRequestViewInfo) prepareViewFillCompareInfo(ctx *context.Conte
|
|||||||
pull := prInfo.issue.PullRequest
|
pull := prInfo.issue.PullRequest
|
||||||
prInfo.CompareInfo, err = git_service.GetCompareInfo(ctx, ctx.Repo.Repository, ctx.Repo.Repository, ctx.Repo.GitRepo, baseRef, git.RefName(pull.GetGitHeadRefName()), false, false)
|
prInfo.CompareInfo, err = git_service.GetCompareInfo(ctx, ctx.Repo.Repository, ctx.Repo.Repository, ctx.Repo.GitRepo, baseRef, git.RefName(pull.GetGitHeadRefName()), false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
isKnownErrorForBroken := errors.Is(err, util.ErrNotExist) || gitcmd.IsStderr(err, gitcmd.StderrNotValidObjectName) || gitcmd.IsStderr(err, gitcmd.StderrUnknownRevisionOrPath)
|
if !errors.Is(err, util.ErrNotExist) && !gitcmd.IsStderr(err, gitcmd.StderrNotValidObjectName) {
|
||||||
if !isKnownErrorForBroken {
|
|
||||||
log.Error("GetCompareInfo: %v", err)
|
log.Error("GetCompareInfo: %v", err)
|
||||||
}
|
}
|
||||||
prInfo.IsPullRequestBroken = true
|
prInfo.IsPullRequestBroken = true
|
||||||
@@ -885,35 +884,39 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
|
|||||||
}
|
}
|
||||||
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
||||||
|
|
||||||
currentReview, err := issues_model.GetCurrentReview(ctx, ctx.Doer, issue)
|
var numPendingCodeComments int64
|
||||||
if err != nil && !issues_model.IsErrReviewNotExist(err) {
|
var currentPendingReview *issues_model.Review
|
||||||
ctx.ServerError("GetCurrentReview", err)
|
if ctx.Doer != nil {
|
||||||
return
|
currentPendingReview, err = issues_model.GetCurrentReview(ctx, ctx.Doer, issue)
|
||||||
}
|
if err != nil && !issues_model.IsErrReviewNotExist(err) {
|
||||||
numPendingCodeComments := int64(0)
|
ctx.ServerError("GetCurrentReview", err)
|
||||||
if currentReview != nil {
|
|
||||||
numPendingCodeComments, err = issues_model.CountComments(ctx, &issues_model.FindCommentsOptions{
|
|
||||||
Type: issues_model.CommentTypeCode,
|
|
||||||
ReviewID: currentReview.ID,
|
|
||||||
IssueID: issue.ID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("CountComments", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if currentPendingReview != nil {
|
||||||
|
numPendingCodeComments, err = issues_model.CountComments(ctx, &issues_model.FindCommentsOptions{
|
||||||
|
Type: issues_model.CommentTypeCode,
|
||||||
|
ReviewID: currentPendingReview.ID,
|
||||||
|
IssueID: issue.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("CountComments", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ctx.Data["CurrentReview"] = currentReview
|
ctx.Data["CurrentReview"] = currentPendingReview
|
||||||
ctx.Data["PendingCodeCommentNumber"] = numPendingCodeComments
|
ctx.Data["PendingCodeCommentNumber"] = numPendingCodeComments
|
||||||
|
|
||||||
ctx.Data["IsIssuePoster"] = ctx.Doer != nil && issue.IsPoster(ctx.Doer.ID)
|
ctx.Data["IsIssuePoster"] = ctx.Doer != nil && issue.IsPoster(ctx.Doer.ID)
|
||||||
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)
|
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)
|
||||||
|
|
||||||
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
|
||||||
// For files changed page
|
// For files changed page
|
||||||
PrepareBranchList(ctx)
|
PrepareBranchList(ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.Data["AllowedIssueReactions"] = setting.UI.Reactions
|
||||||
upload.AddUploadContext(ctx, "comment")
|
upload.AddUploadContext(ctx, "comment")
|
||||||
|
|
||||||
ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
|
ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ func RenderNewCodeCommentForm(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["AfterCommitID"] = pullHeadCommitID
|
ctx.Data["AfterCommitID"] = pullHeadCommitID
|
||||||
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
|
||||||
upload.AddUploadContext(ctx, "comment")
|
upload.AddUploadContext(ctx, "comment")
|
||||||
ctx.HTML(http.StatusOK, tplNewComment)
|
ctx.HTML(http.StatusOK, tplNewComment)
|
||||||
}
|
}
|
||||||
@@ -186,7 +185,7 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment, ori
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
ctx.Data["AllowedIssueReactions"] = setting.UI.Reactions
|
||||||
upload.AddUploadContext(ctx, "comment")
|
upload.AddUploadContext(ctx, "comment")
|
||||||
|
|
||||||
ctx.Data["comments"] = comments
|
ctx.Data["comments"] = comments
|
||||||
|
|||||||
+85
-13
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"gitea.dev/models/db"
|
"gitea.dev/models/db"
|
||||||
git_model "gitea.dev/models/git"
|
git_model "gitea.dev/models/git"
|
||||||
|
issues_model "gitea.dev/models/issues"
|
||||||
"gitea.dev/models/renderhelper"
|
"gitea.dev/models/renderhelper"
|
||||||
repo_model "gitea.dev/models/repo"
|
repo_model "gitea.dev/models/repo"
|
||||||
"gitea.dev/models/unit"
|
"gitea.dev/models/unit"
|
||||||
@@ -73,6 +74,7 @@ type ReleaseInfo struct {
|
|||||||
Release *repo_model.Release
|
Release *repo_model.Release
|
||||||
CommitStatus *git_model.CommitStatus
|
CommitStatus *git_model.CommitStatus
|
||||||
CommitStatuses []*git_model.CommitStatus
|
CommitStatuses []*git_model.CommitStatus
|
||||||
|
Reactions issues_model.ReactionList
|
||||||
}
|
}
|
||||||
|
|
||||||
func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions) ([]*ReleaseInfo, error) {
|
func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions) ([]*ReleaseInfo, error) {
|
||||||
@@ -81,17 +83,31 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
releaseIDs := make([]int64, 0, len(releases))
|
||||||
for _, release := range releases {
|
for _, release := range releases {
|
||||||
release.Repo = ctx.Repo.Repository
|
release.Repo = ctx.Repo.Repository
|
||||||
|
releaseIDs = append(releaseIDs, release.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = repo_model.GetReleaseAttachments(ctx, releases...); err != nil {
|
if err = repo_model.GetReleaseAttachments(ctx, releases...); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reactions, err := issues_model.FindReleaseReactions(ctx, releaseIDs...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cacheUsers, err := reactions.LoadUsers(ctx, ctx.Repo.Repository)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
reactionsByRelease := make(map[int64]issues_model.ReactionList)
|
||||||
|
for _, reaction := range reactions {
|
||||||
|
reactionsByRelease[reaction.ReleaseID] = append(reactionsByRelease[reaction.ReleaseID], reaction)
|
||||||
|
}
|
||||||
|
|
||||||
// Temporary cache commits count of used branches to speed up.
|
// Temporary cache commits count of used branches to speed up.
|
||||||
countCache := make(map[string]int64)
|
countCache := make(map[string]int64)
|
||||||
cacheUsers := make(map[int64]*user_model.User)
|
|
||||||
if ctx.Doer != nil {
|
if ctx.Doer != nil {
|
||||||
cacheUsers[ctx.Doer.ID] = ctx.Doer
|
cacheUsers[ctx.Doer.ID] = ctx.Doer
|
||||||
}
|
}
|
||||||
@@ -138,7 +154,8 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
|
|||||||
}
|
}
|
||||||
|
|
||||||
info := &ReleaseInfo{
|
info := &ReleaseInfo{
|
||||||
Release: r,
|
Release: r,
|
||||||
|
Reactions: reactionsByRelease[r.ID],
|
||||||
}
|
}
|
||||||
|
|
||||||
if canReadCode {
|
if canReadCode {
|
||||||
@@ -154,10 +171,22 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
|
|||||||
return releaseInfos, nil
|
return releaseInfos, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Releases render releases list page
|
func prepareReleasePageCommon(ctx *context.Context) (ret struct {
|
||||||
func Releases(ctx *context.Context) {
|
writeAccess bool
|
||||||
|
},
|
||||||
|
) {
|
||||||
ctx.Data["PageIsReleaseList"] = true
|
ctx.Data["PageIsReleaseList"] = true
|
||||||
ctx.Data["Title"] = ctx.Tr("repo.release.releases")
|
ctx.Data["Title"] = ctx.Tr("repo.release.releases")
|
||||||
|
ctx.Data["AllowedReleaseReactions"] = release_service.AllowedReleaseReactions()
|
||||||
|
|
||||||
|
ret.writeAccess = ctx.Repo.Permission.CanWrite(unit.TypeReleases)
|
||||||
|
ctx.Data["CanCreateRelease"] = ret.writeAccess && !ctx.Repo.Repository.IsArchived
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// Releases render releases list page
|
||||||
|
func Releases(ctx *context.Context) {
|
||||||
|
preparedData := prepareReleasePageCommon(ctx)
|
||||||
|
|
||||||
listOptions := db.ListOptions{
|
listOptions := db.ListOptions{
|
||||||
Page: ctx.FormInt("page"),
|
Page: ctx.FormInt("page"),
|
||||||
@@ -170,13 +199,10 @@ func Releases(ctx *context.Context) {
|
|||||||
listOptions.PageSize = setting.API.MaxResponseItems
|
listOptions.PageSize = setting.API.MaxResponseItems
|
||||||
}
|
}
|
||||||
|
|
||||||
writeAccess := ctx.Repo.Permission.CanWrite(unit.TypeReleases)
|
|
||||||
ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
|
|
||||||
|
|
||||||
releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{
|
releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{
|
||||||
ListOptions: listOptions,
|
ListOptions: listOptions,
|
||||||
// only show draft releases for users who can write, read-only users shouldn't see draft releases.
|
// only show draft releases for users who can write, read-only users shouldn't see draft releases.
|
||||||
IncludeDrafts: writeAccess,
|
IncludeDrafts: preparedData.writeAccess,
|
||||||
RepoID: ctx.Repo.Repository.ID,
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -275,17 +301,14 @@ func releasesOrTagsFeed(ctx *context.Context, isReleasesOnly bool, formatType st
|
|||||||
|
|
||||||
// SingleRelease renders a single release's page
|
// SingleRelease renders a single release's page
|
||||||
func SingleRelease(ctx *context.Context) {
|
func SingleRelease(ctx *context.Context) {
|
||||||
ctx.Data["PageIsReleaseList"] = true
|
preparedData := prepareReleasePageCommon(ctx)
|
||||||
|
|
||||||
writeAccess := ctx.Repo.Permission.CanWrite(unit.TypeReleases)
|
|
||||||
ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
|
|
||||||
|
|
||||||
releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{
|
releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{
|
||||||
ListOptions: db.ListOptions{Page: 1, PageSize: 1},
|
ListOptions: db.ListOptions{Page: 1, PageSize: 1},
|
||||||
RepoID: ctx.Repo.Repository.ID,
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
TagNames: []string{ctx.PathParam("*")},
|
TagNames: []string{ctx.PathParam("*")},
|
||||||
// only show draft releases for users who can write, read-only users shouldn't see draft releases.
|
// only show draft releases for users who can write, read-only users shouldn't see draft releases.
|
||||||
IncludeDrafts: writeAccess,
|
IncludeDrafts: preparedData.writeAccess,
|
||||||
IncludeTags: true,
|
IncludeTags: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -681,3 +704,52 @@ func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) {
|
|||||||
|
|
||||||
redirect()
|
redirect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ChangeReleaseReaction(ctx *context.Context) {
|
||||||
|
form := context.GetFetchActionForm[*forms.ReactionForm](ctx)
|
||||||
|
if form == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rel, err := repo_model.GetReleaseForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetReleaseForRepoByID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if rel.IsTag || rel.IsDraft {
|
||||||
|
ctx.NotFound(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rel.Repo = ctx.Repo.Repository
|
||||||
|
|
||||||
|
switch ctx.PathParam("action") {
|
||||||
|
case "react":
|
||||||
|
_, err = release_service.CreateReleaseReaction(ctx, ctx.Doer, rel, form.Content)
|
||||||
|
if errors.Is(err, util.ErrPermissionDenied) {
|
||||||
|
ctx.HTTPError(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
} else if err != nil && !errors.Is(err, util.ErrAlreadyExist) {
|
||||||
|
ctx.ServerError("CreateReleaseReaction", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "unreact":
|
||||||
|
if err = issues_model.DeleteReleaseReaction(ctx, ctx.Doer.ID, rel.ID, form.Content); err != nil {
|
||||||
|
ctx.ServerError("DeleteReleaseReaction", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
ctx.NotFound(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reactions, err := issues_model.FindReleaseReactions(ctx, rel.ID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("FindReleaseReactions", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err = reactions.LoadUsers(ctx, ctx.Repo.Repository); err != nil {
|
||||||
|
ctx.ServerError("LoadUsers", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
renderReactions(ctx, fmt.Sprintf("%s/releases/%d/reactions", ctx.Repo.RepoLink, rel.ID), reactions, release_service.AllowedReleaseReactions())
|
||||||
|
}
|
||||||
@@ -680,6 +680,7 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
|
|||||||
DefaultUpdateStyle: defaultUpdateStyle,
|
DefaultUpdateStyle: defaultUpdateStyle,
|
||||||
DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge,
|
DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge,
|
||||||
DefaultMergeStyle: repo_model.MergeStyle(form.PullsDefaultMergeStyle),
|
DefaultMergeStyle: repo_model.MergeStyle(form.PullsDefaultMergeStyle),
|
||||||
|
DefaultSquashMergeMessage: util.IfZero(repo_model.SquashMergeMessage(form.PullsDefaultSquashMergeMessage), repo_model.DefaultPullRequestsConfig().DefaultSquashMergeMessage),
|
||||||
DefaultAllowMaintainerEdit: form.DefaultAllowMaintainerEdit,
|
DefaultAllowMaintainerEdit: form.DefaultAllowMaintainerEdit,
|
||||||
DefaultTargetBranch: strings.TrimSpace(form.DefaultTargetBranch),
|
DefaultTargetBranch: strings.TrimSpace(form.DefaultTargetBranch),
|
||||||
}
|
}
|
||||||
@@ -687,6 +688,10 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
|
|||||||
ctx.JSONErrorAuto(err)
|
ctx.JSONErrorAuto(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !prConfig.DefaultSquashMergeMessage.IsValid() {
|
||||||
|
ctx.JSONErrorAuto(util.NewInvalidArgumentErrorf("invalid default squash merge message: %s", prConfig.DefaultSquashMergeMessage))
|
||||||
|
return
|
||||||
|
}
|
||||||
units = append(units, newRepoUnit(repo, unit_model.TypePullRequests, prConfig))
|
units = append(units, newRepoUnit(repo, unit_model.TypePullRequests, prConfig))
|
||||||
} else if !unit_model.TypePullRequests.UnitGlobalDisabled() {
|
} else if !unit_model.TypePullRequests.UnitGlobalDisabled() {
|
||||||
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests)
|
deleteUnitTypes = append(deleteUnitTypes, unit_model.TypePullRequests)
|
||||||
|
|||||||
+4
-3
@@ -224,13 +224,13 @@ func verifyAuthWithOptionsWeb(options *common.VerifyOptions) func(ctx *context.C
|
|||||||
}
|
}
|
||||||
|
|
||||||
if options.SignInRequired && !ctx.IsSigned {
|
if options.SignInRequired && !ctx.IsSigned {
|
||||||
ctx.Redirect(middleware.RedirectLinkUserLogin(ctx.Req))
|
ctx.RedirectForLogin(middleware.RedirectLinkUserLogin(ctx.Req))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to log in page if auto-signin info is provided and has not signed in.
|
// Redirect to log in page if auto-signin info is provided and has not signed in.
|
||||||
if !options.SignOutRequired && !ctx.IsSigned && ctx.GetSiteCookie(setting.CookieRememberName) != "" {
|
if !options.SignOutRequired && !ctx.IsSigned && ctx.GetSiteCookie(setting.CookieRememberName) != "" {
|
||||||
ctx.Redirect(middleware.RedirectLinkUserLogin(ctx.Req))
|
ctx.RedirectForLogin(middleware.RedirectLinkUserLogin(ctx.Req))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1503,6 +1503,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
|||||||
}, ctxDataSet(reqctx.ContextData{"EnableFeed": setting.Other.EnableFeed}))
|
}, ctxDataSet(reqctx.ContextData{"EnableFeed": setting.Other.EnableFeed}))
|
||||||
m.Get("/releases/attachments/{uuid}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.GetAttachment)
|
m.Get("/releases/attachments/{uuid}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.GetAttachment)
|
||||||
m.Get("/releases/download/{vTag}/{fileName}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.RedirectDownload)
|
m.Get("/releases/download/{vTag}/{fileName}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.RedirectDownload)
|
||||||
|
m.Post("/releases/{id}/reactions/{action}", reqSignIn, context.RepoMustNotBeArchived(), repo.ChangeReleaseReaction)
|
||||||
m.Group("/releases", func() {
|
m.Group("/releases", func() {
|
||||||
m.Get("/new", repo.NewRelease)
|
m.Get("/new", repo.NewRelease)
|
||||||
m.Post("/new", web.Bind[*forms.NewReleaseForm](), repo.NewReleasePost)
|
m.Post("/new", web.Bind[*forms.NewReleaseForm](), repo.NewReleasePost)
|
||||||
@@ -1661,7 +1662,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
|||||||
m.Get("/new_comment", repo.RenderNewCodeCommentForm)
|
m.Get("/new_comment", repo.RenderNewCodeCommentForm)
|
||||||
m.Post("/comments", web.Bind[*forms.CodeCommentForm](), repo.SetShowOutdatedComments, repo.CreateCodeComment)
|
m.Post("/comments", web.Bind[*forms.CodeCommentForm](), repo.SetShowOutdatedComments, repo.CreateCodeComment)
|
||||||
m.Post("/submit", web.Bind[*forms.SubmitReviewForm](), repo.SubmitReview)
|
m.Post("/submit", web.Bind[*forms.SubmitReviewForm](), repo.SubmitReview)
|
||||||
}, context.RepoMustNotBeArchived())
|
}, reqSignIn, context.RepoMustNotBeArchived())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}, optSignIn, context.RepoAssignment, repo.MustAllowPulls, reqUnitPullsReader)
|
}, optSignIn, context.RepoAssignment, repo.MustAllowPulls, reqUnitPullsReader)
|
||||||
|
|||||||
@@ -148,16 +148,26 @@ func (b *Base) PlainText(status int, text string) {
|
|||||||
|
|
||||||
// Redirect redirects the request
|
// Redirect redirects the request
|
||||||
func (b *Base) Redirect(location string, status ...int) {
|
func (b *Base) Redirect(location string, status ...int) {
|
||||||
code := util.OptionalArg(status, http.StatusSeeOther)
|
|
||||||
// In case the request is made by "fetch-action" module, make JS redirect to the new location
|
// In case the request is made by "fetch-action" module, make JS redirect to the new location
|
||||||
// Otherwise, the JS fetch will follow the redirection and read a "login" page, embed it to the current page, which is not expected.
|
// Otherwise, the JS fetch will follow the redirection and read a "login" page, embed it to the current page, which is not expected.
|
||||||
if httplib.IsGiteaFetchActionRequest(b.Req) {
|
if httplib.IsGiteaFetchActionRequest(b.Req) {
|
||||||
b.JSON(http.StatusOK, map[string]any{"redirect": location})
|
b.JSON(http.StatusOK, map[string]any{"redirect": location})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
code := util.OptionalArg(status, http.StatusSeeOther)
|
||||||
http.Redirect(b.Resp, b.Req, location, code)
|
http.Redirect(b.Resp, b.Req, location, code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Base) RedirectForLogin(location string) {
|
||||||
|
if httplib.IsGiteaFetchActionRequest(b.Req) {
|
||||||
|
// let fetch-action handles the redirect automatically
|
||||||
|
b.JSON(http.StatusUnauthorized, map[string]any{"redirect": location})
|
||||||
|
} else {
|
||||||
|
// keep the old behaivor, use 303 to redirect to login page
|
||||||
|
http.Redirect(b.Resp, b.Req, location, http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type ServeHeaderOptions = httplib.ServeHeaderOptions
|
type ServeHeaderOptions = httplib.ServeHeaderOptions
|
||||||
|
|
||||||
func (b *Base) SetServeHeaders(opts ServeHeaderOptions) {
|
func (b *Base) SetServeHeaders(opts ServeHeaderOptions) {
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ type uploadOptions struct {
|
|||||||
|
|
||||||
// AddUploadContext renders template values for dropzone
|
// AddUploadContext renders template values for dropzone
|
||||||
func AddUploadContext(ctx *context.Context, uploadType string) {
|
func AddUploadContext(ctx *context.Context, uploadType string) {
|
||||||
|
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
||||||
switch uploadType {
|
switch uploadType {
|
||||||
case "release":
|
case "release":
|
||||||
ctx.Data["UploadOptions"] = uploadOptions{
|
ctx.Data["UploadOptions"] = uploadOptions{
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
|||||||
defaultDeleteBranchAfterMerge := false
|
defaultDeleteBranchAfterMerge := false
|
||||||
defaultMergeStyle := repo_model.MergeStyleMerge
|
defaultMergeStyle := repo_model.MergeStyleMerge
|
||||||
defaultUpdateStyle := repo_model.UpdateStyleMerge
|
defaultUpdateStyle := repo_model.UpdateStyleMerge
|
||||||
|
defaultSquashMergeMessage := repo_model.DefaultPullRequestsConfig().DefaultSquashMergeMessage
|
||||||
defaultAllowMaintainerEdit := false
|
defaultAllowMaintainerEdit := false
|
||||||
defaultTargetBranch := ""
|
defaultTargetBranch := ""
|
||||||
if unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests); err == nil {
|
if unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests); err == nil {
|
||||||
@@ -123,6 +124,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
|||||||
defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge
|
defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge
|
||||||
defaultMergeStyle = config.DefaultMergeStyle
|
defaultMergeStyle = config.DefaultMergeStyle
|
||||||
defaultUpdateStyle = config.DefaultUpdateStyle
|
defaultUpdateStyle = config.DefaultUpdateStyle
|
||||||
|
defaultSquashMergeMessage = config.DefaultSquashMergeMessage
|
||||||
defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit
|
defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit
|
||||||
defaultTargetBranch = config.DefaultTargetBranch
|
defaultTargetBranch = config.DefaultTargetBranch
|
||||||
}
|
}
|
||||||
@@ -251,6 +253,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
|||||||
DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge,
|
DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge,
|
||||||
DefaultMergeStyle: string(defaultMergeStyle),
|
DefaultMergeStyle: string(defaultMergeStyle),
|
||||||
DefaultUpdateStyle: string(defaultUpdateStyle),
|
DefaultUpdateStyle: string(defaultUpdateStyle),
|
||||||
|
SquashMergeCommitMessage: string(defaultSquashMergeMessage),
|
||||||
DefaultAllowMaintainerEdit: defaultAllowMaintainerEdit,
|
DefaultAllowMaintainerEdit: defaultAllowMaintainerEdit,
|
||||||
DefaultTargetBranch: defaultTargetBranch,
|
DefaultTargetBranch: defaultTargetBranch,
|
||||||
AvatarURL: repo.AvatarLink(ctx),
|
AvatarURL: repo.AvatarLink(ctx),
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ type RepoSettingForm struct {
|
|||||||
PullsAllowMergeUpdate bool
|
PullsAllowMergeUpdate bool
|
||||||
PullsAllowRebaseUpdate bool
|
PullsAllowRebaseUpdate bool
|
||||||
PullsDefaultUpdateStyle string
|
PullsDefaultUpdateStyle string
|
||||||
|
PullsDefaultSquashMergeMessage string
|
||||||
DefaultDeleteBranchAfterMerge bool
|
DefaultDeleteBranchAfterMerge bool
|
||||||
DefaultAllowMaintainerEdit bool
|
DefaultAllowMaintainerEdit bool
|
||||||
DefaultTargetBranch string
|
DefaultTargetBranch string
|
||||||
@@ -424,20 +425,21 @@ type MergePullRequestForm struct {
|
|||||||
ForceMerge bool `json:"force_merge,omitempty"`
|
ForceMerge bool `json:"force_merge,omitempty"`
|
||||||
MergeWhenChecksSucceed bool `json:"merge_when_checks_succeed,omitempty"`
|
MergeWhenChecksSucceed bool `json:"merge_when_checks_succeed,omitempty"`
|
||||||
DeleteBranchAfterMerge *bool `json:"delete_branch_after_merge,omitempty"`
|
DeleteBranchAfterMerge *bool `json:"delete_branch_after_merge,omitempty"`
|
||||||
|
HasMergeMessageField bool `form:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *MergePullRequestForm) UnmarshalJSON(b []byte) error {
|
func (f *MergePullRequestForm) UnmarshalJSON(b []byte) error {
|
||||||
// This is for backward compatibility, to support both field names like "do" and "Do",
|
// This is for backward compatibility, to support both field names like "do" and "Do",
|
||||||
// because old code doesn't have "json" tag for these fields
|
// because old code doesn't have "json" tag for these fields
|
||||||
type aux struct {
|
type aux struct {
|
||||||
Do1 string `json:"do"`
|
Do1 string `json:"do"`
|
||||||
Do2 string `json:"Do"`
|
Do2 string `json:"Do"`
|
||||||
MergeTitleField1 string `json:"merge_title_field"`
|
MergeTitleField1 string `json:"merge_title_field"`
|
||||||
MergeTitleField2 string `json:"MergeTitleField"`
|
MergeTitleField2 string `json:"MergeTitleField"`
|
||||||
MergeMessageField1 string `json:"merge_message_field"`
|
MergeMessageField1 *string `json:"merge_message_field"`
|
||||||
MergeMessageField2 string `json:"MergeMessageField"`
|
MergeMessageField2 *string `json:"MergeMessageField"`
|
||||||
MergeCommitID1 string `json:"merge_commit_id"`
|
MergeCommitID1 string `json:"merge_commit_id"`
|
||||||
MergeCommitID2 string `json:"MergeCommitID"`
|
MergeCommitID2 string `json:"MergeCommitID"`
|
||||||
|
|
||||||
HeadCommitID string `json:"head_commit_id"`
|
HeadCommitID string `json:"head_commit_id"`
|
||||||
ForceMerge bool `json:"force_merge"`
|
ForceMerge bool `json:"force_merge"`
|
||||||
@@ -450,7 +452,10 @@ func (f *MergePullRequestForm) UnmarshalJSON(b []byte) error {
|
|||||||
}
|
}
|
||||||
f.Do = util.IfZero(a.Do1, a.Do2)
|
f.Do = util.IfZero(a.Do1, a.Do2)
|
||||||
f.MergeTitleField = util.IfZero(a.MergeTitleField1, a.MergeTitleField2)
|
f.MergeTitleField = util.IfZero(a.MergeTitleField1, a.MergeTitleField2)
|
||||||
f.MergeMessageField = util.IfZero(a.MergeMessageField1, a.MergeMessageField2)
|
f.MergeMessageField, f.HasMergeMessageField = "", false
|
||||||
|
if mergeMessageField := util.IfZero(a.MergeMessageField1, a.MergeMessageField2); mergeMessageField != nil {
|
||||||
|
f.MergeMessageField, f.HasMergeMessageField = *mergeMessageField, true
|
||||||
|
}
|
||||||
f.MergeCommitID = util.IfZero(a.MergeCommitID1, a.MergeCommitID2)
|
f.MergeCommitID = util.IfZero(a.MergeCommitID1, a.MergeCommitID2)
|
||||||
f.HeadCommitID = a.HeadCommitID
|
f.HeadCommitID = a.HeadCommitID
|
||||||
f.ForceMerge = a.ForceMerge
|
f.ForceMerge = a.ForceMerge
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ func TestMergePullRequestForm(t *testing.T) {
|
|||||||
ForceMerge: true,
|
ForceMerge: true,
|
||||||
MergeWhenChecksSucceed: true,
|
MergeWhenChecksSucceed: true,
|
||||||
DeleteBranchAfterMerge: new(true),
|
DeleteBranchAfterMerge: new(true),
|
||||||
|
HasMergeMessageField: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("NewFields", func(t *testing.T) {
|
t.Run("NewFields", func(t *testing.T) {
|
||||||
@@ -84,4 +85,18 @@ func TestMergePullRequestForm(t *testing.T) {
|
|||||||
require.NoError(t, json.Unmarshal([]byte(input), &m))
|
require.NoError(t, json.Unmarshal([]byte(input), &m))
|
||||||
assert.Equal(t, expected, m)
|
assert.Equal(t, expected, m)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("OmittedMergeMessageFieldClearsPreviousMessage", func(t *testing.T) {
|
||||||
|
form := &MergePullRequestForm{MergeMessageField: "previous message", HasMergeMessageField: true}
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(`{"do": "squash"}`), &form))
|
||||||
|
assert.Empty(t, form.MergeMessageField)
|
||||||
|
assert.False(t, form.HasMergeMessageField)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("EmptyMergeMessageField", func(t *testing.T) {
|
||||||
|
var form MergePullRequestForm
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(`{"do": "squash", "merge_message_field": ""}`), &form))
|
||||||
|
assert.Empty(t, form.MergeMessageField)
|
||||||
|
assert.True(t, form.HasMergeMessageField)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@@ -1596,6 +1596,22 @@ func CommentAsDiff(ctx context.Context, c *issues_model.Comment) (*Diff, error)
|
|||||||
return diff, nil
|
return diff, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCodeCommentPatch returns the code snippet stored with a code comment
|
||||||
|
func GetCodeCommentPatch(ctx context.Context, gitRepo *git.Repository, mergeBase, headCommitID, treePath string, line int64) (string, error) {
|
||||||
|
patch, err := git.GetFileDiffCutAroundLine(ctx, gitRepo, mergeBase, headCommitID, treePath,
|
||||||
|
int64((&issues_model.Comment{Line: line}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines,
|
||||||
|
)
|
||||||
|
contextCommitID := util.Iif(line < 0, mergeBase, headCommitID)
|
||||||
|
if err != nil || patch != "" || contextCommitID == "" {
|
||||||
|
return patch, err
|
||||||
|
}
|
||||||
|
patch, err = GeneratePatchForUnchangedLine(ctx, gitRepo, contextCommitID, treePath, line, setting.UI.CodeCommentLines)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("Unable to generate patch for unchanged line (file=%s, line=%d, commit=%s): %v", treePath, line, contextCommitID, err)
|
||||||
|
}
|
||||||
|
return patch, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GeneratePatchForUnchangedLine creates a patch showing code context for an unchanged line
|
// GeneratePatchForUnchangedLine creates a patch showing code context for an unchanged line
|
||||||
func GeneratePatchForUnchangedLine(ctx context.Context, gitRepo *git.Repository, commitID, treePath string, line int64, contextLines int) (string, error) {
|
func GeneratePatchForUnchangedLine(ctx context.Context, gitRepo *git.Repository, commitID, treePath string, line int64, contextLines int) (string, error) {
|
||||||
commit, err := gitRepo.GetCommit(ctx, commitID)
|
commit, err := gitRepo.GetCommit(ctx, commitID)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
issues_model "gitea.dev/models/issues"
|
issues_model "gitea.dev/models/issues"
|
||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
|
"gitea.dev/modules/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateIssueReaction creates a reaction on an issue.
|
// CreateIssueReaction creates a reaction on an issue.
|
||||||
@@ -20,7 +21,7 @@ func CreateIssueReaction(ctx context.Context, doer *user_model.User, issue *issu
|
|||||||
return nil, user_model.ErrBlockedUser
|
return nil, user_model.ErrBlockedUser
|
||||||
}
|
}
|
||||||
|
|
||||||
return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
|
return issues_model.CreateReaction(ctx, setting.UI.ReactionsLookup, &issues_model.ReactionOptions{
|
||||||
Type: content,
|
Type: content,
|
||||||
DoerID: doer.ID,
|
DoerID: doer.ID,
|
||||||
IssueID: issue.ID,
|
IssueID: issue.ID,
|
||||||
@@ -41,7 +42,7 @@ func CreateCommentReaction(ctx context.Context, doer *user_model.User, comment *
|
|||||||
return nil, user_model.ErrBlockedUser
|
return nil, user_model.ErrBlockedUser
|
||||||
}
|
}
|
||||||
|
|
||||||
return issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
|
return issues_model.CreateReaction(ctx, setting.UI.ReactionsLookup, &issues_model.ReactionOptions{
|
||||||
Type: content,
|
Type: content,
|
||||||
DoerID: doer.ID,
|
DoerID: doer.ID,
|
||||||
IssueID: comment.Issue.ID,
|
IssueID: comment.Issue.ID,
|
||||||
|
|||||||
@@ -90,9 +90,7 @@ func TestIssueReactionCount(t *testing.T) {
|
|||||||
addReaction(t, user4, issue, nil, "heart")
|
addReaction(t, user4, issue, nil, "heart")
|
||||||
addReaction(t, ghost, issue, nil, "-1")
|
addReaction(t, ghost, issue, nil, "-1")
|
||||||
|
|
||||||
reactionsList, _, err := issues_model.FindReactions(t.Context(), issues_model.FindReactionsOptions{
|
reactionsList, _, err := issues_model.FindReactions(t.Context(), issues_model.FindReactionsOptions{IssueIDOnly: issue.ID})
|
||||||
IssueID: issue.ID,
|
|
||||||
})
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, reactionsList, 7)
|
assert.Len(t, reactionsList, 7)
|
||||||
_, err = reactionsList.LoadUsers(t.Context(), repo)
|
_, err = reactionsList.LoadUsers(t.Context(), repo)
|
||||||
@@ -101,7 +99,7 @@ func TestIssueReactionCount(t *testing.T) {
|
|||||||
reactions := reactionsList.GroupByType()
|
reactions := reactionsList.GroupByType()
|
||||||
assert.Len(t, reactions["heart"], 4)
|
assert.Len(t, reactions["heart"], 4)
|
||||||
assert.Equal(t, 2, reactions["heart"].GetMoreUserCount())
|
assert.Equal(t, 2, reactions["heart"].GetMoreUserCount())
|
||||||
assert.Equal(t, user1.Name+", "+user2.Name, reactions["heart"].GetFirstUsers())
|
assert.Equal(t, "user1 (User One), user2 (< U<se>r Tw<o > ><)", reactions["heart"].GetFirstUsers())
|
||||||
assert.True(t, reactions["heart"].HasUser(1))
|
assert.True(t, reactions["heart"].HasUser(1))
|
||||||
assert.False(t, reactions["heart"].HasUser(5))
|
assert.False(t, reactions["heart"].HasUser(5))
|
||||||
assert.False(t, reactions["heart"].HasUser(0))
|
assert.False(t, reactions["heart"].HasUser(0))
|
||||||
@@ -136,10 +134,7 @@ func TestIssueCommentDeleteReaction(t *testing.T) {
|
|||||||
addReaction(t, org3, nil, comment, "heart")
|
addReaction(t, org3, nil, comment, "heart")
|
||||||
addReaction(t, user4, nil, comment, "+1")
|
addReaction(t, user4, nil, comment, "+1")
|
||||||
|
|
||||||
reactionsList, _, err := issues_model.FindReactions(t.Context(), issues_model.FindReactionsOptions{
|
reactionsList, _, err := issues_model.FindReactions(t.Context(), issues_model.FindReactionsOptions{CommentID: comment.ID})
|
||||||
IssueID: comment.IssueID,
|
|
||||||
CommentID: comment.ID,
|
|
||||||
})
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, reactionsList, 4)
|
assert.Len(t, reactionsList, 4)
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import (
|
|||||||
"gitea.dev/modules/timeutil"
|
"gitea.dev/modules/timeutil"
|
||||||
"gitea.dev/modules/uri"
|
"gitea.dev/modules/uri"
|
||||||
"gitea.dev/modules/util"
|
"gitea.dev/modules/util"
|
||||||
|
"gitea.dev/services/gitdiff"
|
||||||
"gitea.dev/services/pull"
|
"gitea.dev/services/pull"
|
||||||
repo_service "gitea.dev/services/repository"
|
repo_service "gitea.dev/services/repository"
|
||||||
)
|
)
|
||||||
@@ -905,10 +906,7 @@ func (g *GiteaLocalUploader) CreateReviews(ctx context.Context, reviews ...*base
|
|||||||
// SECURITY: The TreePath must be cleaned! use relative path
|
// SECURITY: The TreePath must be cleaned! use relative path
|
||||||
comment.TreePath = util.PathJoinRel(comment.TreePath)
|
comment.TreePath = util.PathJoinRel(comment.TreePath)
|
||||||
|
|
||||||
patch, _ := git.GetFileDiffCutAroundLine(ctx,
|
patch, _ := gitdiff.GetCodeCommentPatch(ctx, g.gitRepo, pr.MergeBase, headCommitID, comment.TreePath, int64(line+comment.Position-1))
|
||||||
g.gitRepo, pr.MergeBase, headCommitID, comment.TreePath,
|
|
||||||
int64((&issues_model.Comment{Line: int64(line + comment.Position - 1)}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines,
|
|
||||||
)
|
|
||||||
|
|
||||||
if comment.CreatedAt.IsZero() {
|
if comment.CreatedAt.IsZero() {
|
||||||
comment.CreatedAt = review.CreatedAt
|
comment.CreatedAt = review.CreatedAt
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = git.ManagedRemoteAdd(ctx, repo, remoteName, addr, git.RemoteOptionMirrorFetch)
|
err = git.ManagedRemoteAdd(ctx, repo, remoteName, addr, git.RemoteOptionMirrorFetch)
|
||||||
if err != nil && !git.IsRemoteNotExistError(err) {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = git.ManagedRemoteAdd(ctx, repo.WikiStorageRepo(), remoteName, wikiRemotePath, git.RemoteOptionMirrorFetch)
|
err = git.ManagedRemoteAdd(ctx, repo.WikiStorageRepo(), remoteName, wikiRemotePath, git.RemoteOptionMirrorFetch)
|
||||||
if err != nil && !git.IsRemoteNotExistError(err) {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-26
@@ -358,26 +358,6 @@ func ChangeTargetBranch(ctx context.Context, pr *issues_model.PullRequest, doer
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkForInvalidation(ctx context.Context, requests issues_model.PullRequestList, repoID int64, doer *user_model.User, branch string) error {
|
|
||||||
repo, err := repo_model.GetRepositoryByID(ctx, repoID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("GetRepositoryByIDCtx: %w", err)
|
|
||||||
}
|
|
||||||
gitRepo, err := git.OpenRepository(ctx, repo)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("gitrepo.OpenRepository: %w", err)
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
// FIXME: graceful: We need to tell the manager we're doing something...
|
|
||||||
err := InvalidateCodeComments(ctx, requests, doer, repo, gitRepo, branch)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("PullRequestList.InvalidateCodeComments: %v", err)
|
|
||||||
}
|
|
||||||
gitRepo.Close()
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type TestPullRequestOptions struct {
|
type TestPullRequestOptions struct {
|
||||||
RepoID int64
|
RepoID int64
|
||||||
Doer *user_model.User
|
Doer *user_model.User
|
||||||
@@ -436,8 +416,8 @@ func AddTestPullRequestTask(opts TestPullRequestOptions) {
|
|||||||
if err = headBranchPRs.LoadAttributes(ctx); err != nil {
|
if err = headBranchPRs.LoadAttributes(ctx); err != nil {
|
||||||
log.Error("PullRequestList.LoadAttributes: %v", err)
|
log.Error("PullRequestList.LoadAttributes: %v", err)
|
||||||
}
|
}
|
||||||
if invalidationErr := checkForInvalidation(ctx, headBranchPRs, opts.RepoID, opts.Doer, opts.Branch); invalidationErr != nil {
|
if invalidationErr := InvalidateCodeComments(ctx, headBranchPRs, repo, opts.Branch); invalidationErr != nil {
|
||||||
log.Error("checkForInvalidation: %v", invalidationErr)
|
log.Error("InvalidateCodeComments: %v", invalidationErr)
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, pr := range headBranchPRs {
|
for _, pr := range headBranchPRs {
|
||||||
@@ -781,9 +761,15 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeMessage := strings.TrimSpace(pr.Issue.Content) // use PR's title and description as squash commit message
|
if err := pr.LoadBaseRepo(ctx); err != nil {
|
||||||
if setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages {
|
return "", err
|
||||||
mergeMessage = formatSquashMergeCommitMessages(limitedCommits) // use PR's commit messages as squash commit message
|
}
|
||||||
|
var mergeMessage string
|
||||||
|
switch pr.BaseRepo.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig().DefaultSquashMergeMessage {
|
||||||
|
case repo_model.SquashMergeMessageCommitMessages:
|
||||||
|
mergeMessage = formatSquashMergeCommitMessages(limitedCommits)
|
||||||
|
case repo_model.SquashMergeMessagePRBody:
|
||||||
|
mergeMessage = strings.TrimSpace(pr.Issue.Content)
|
||||||
}
|
}
|
||||||
coAuthors := collectSquashMergeCommitCoAuthors(ctx, gitRepo, pr, headCommitRef, mergeBaseRef, limit, limitedCommits)
|
coAuthors := collectSquashMergeCommitCoAuthors(ctx, gitRepo, pr, headCommitRef, mergeBaseRef, limit, limitedCommits)
|
||||||
return buildSquashMergeCommitMessages(mergeMessage, coAuthors), nil
|
return buildSquashMergeCommitMessages(mergeMessage, coAuthors), nil
|
||||||
@@ -797,7 +783,7 @@ func buildSquashMergeCommitMessages(mergeMessage string, coAuthors []string) str
|
|||||||
msgContent, msgSep, msgTrailer := git.CommitMessageSplitTrailer(mergeMessage)
|
msgContent, msgSep, msgTrailer := git.CommitMessageSplitTrailer(mergeMessage)
|
||||||
if (msgSep == "" || msgSep == "\n\n") && msgTrailer == "" {
|
if (msgSep == "" || msgSep == "\n\n") && msgTrailer == "" {
|
||||||
msgContent = strings.TrimRightFunc(msgContent, unicode.IsSpace)
|
msgContent = strings.TrimRightFunc(msgContent, unicode.IsSpace)
|
||||||
msgSep = "\n\n---------\n\n"
|
msgSep = util.Iif(msgContent == "", "", "\n\n---------\n\n")
|
||||||
}
|
}
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
sb.WriteString(msgContent)
|
sb.WriteString(msgContent)
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ func TestBuildSquashMergeCommitMessages(t *testing.T) {
|
|||||||
coAuthors []string
|
coAuthors []string
|
||||||
expected string
|
expected string
|
||||||
}{
|
}{
|
||||||
|
{"", []string{"the-user"}, "Co-authored-by: the-user\n"},
|
||||||
{"title", nil, "title"},
|
{"title", nil, "title"},
|
||||||
{"title", []string{"the-user"}, "title\n\n---------\n\nCo-authored-by: the-user\n"},
|
{"title", []string{"the-user"}, "title\n\n---------\n\nCo-authored-by: the-user\n"},
|
||||||
{"title\n\n", []string{"the-user"}, "title\n\n---------\n\nCo-authored-by: the-user\n"},
|
{"title\n\n", []string{"the-user"}, "title\n\n---------\n\nCo-authored-by: the-user\n"},
|
||||||
|
|||||||
+77
-99
@@ -17,14 +17,13 @@ import (
|
|||||||
"gitea.dev/modules/git/gitcmd"
|
"gitea.dev/modules/git/gitcmd"
|
||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
"gitea.dev/modules/optional"
|
"gitea.dev/modules/optional"
|
||||||
"gitea.dev/modules/setting"
|
|
||||||
"gitea.dev/modules/util"
|
"gitea.dev/modules/util"
|
||||||
"gitea.dev/services/gitdiff"
|
"gitea.dev/services/gitdiff"
|
||||||
notify_service "gitea.dev/services/notify"
|
notify_service "gitea.dev/services/notify"
|
||||||
)
|
)
|
||||||
|
|
||||||
func isErrBlameNotFoundOrNotEnoughLines(err error) bool {
|
func isErrBlameInvalidated(err error) bool {
|
||||||
return gitcmd.IsStderr(err, gitcmd.StderrNoSuchPath, gitcmd.StderrFileNoEnoughLines)
|
return gitcmd.IsStderr(err, gitcmd.StderrNoSuchPath, gitcmd.StderrFileNoEnoughLines, gitcmd.StderrBadObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrDismissRequestOnClosedPR represents an error when a user tries to dismiss a review associated to a closed or merged PR.
|
// ErrDismissRequestOnClosedPR represents an error when a user tries to dismiss a review associated to a closed or merged PR.
|
||||||
@@ -47,41 +46,27 @@ func (err ErrDismissRequestOnClosedPR) Unwrap() error {
|
|||||||
// ErrSubmitReviewOnClosedPR represents an error when a user tries to submit an approve or reject review associated to a closed or merged PR.
|
// ErrSubmitReviewOnClosedPR represents an error when a user tries to submit an approve or reject review associated to a closed or merged PR.
|
||||||
var ErrSubmitReviewOnClosedPR = errors.New("can't submit review for a closed or merged PR")
|
var ErrSubmitReviewOnClosedPR = errors.New("can't submit review for a closed or merged PR")
|
||||||
|
|
||||||
// LineBlame returns the latest commit at the given line
|
|
||||||
func lineBlame(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, branch, file string, line uint) (*git.Commit, error) {
|
|
||||||
sha, err := git.LineBlame(ctx, repo, branch, file, line)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(sha) < 40 {
|
|
||||||
return nil, fmt.Errorf("invalid result of blame: %s", sha)
|
|
||||||
}
|
|
||||||
|
|
||||||
objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
|
|
||||||
return gitRepo.GetCommit(ctx, sha[:objectFormat.FullLength()])
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkInvalidation checks if the line of code comment got changed by another commit.
|
// checkInvalidation checks if the line of code comment got changed by another commit.
|
||||||
// If the line got changed the comment is going to be invalidated.
|
// If the line got changed the comment is going to be invalidated.
|
||||||
func checkInvalidation(ctx context.Context, c *issues_model.Comment, repo *repo_model.Repository, gitRepo *git.Repository, branch string) error {
|
func checkInvalidation(ctx context.Context, c *issues_model.Comment, repo *repo_model.Repository, branch string) error {
|
||||||
// FIXME differentiate between previous and proposed line
|
// FIXME differentiate between previous and proposed line
|
||||||
commit, err := lineBlame(ctx, repo, gitRepo, branch, c.TreePath, uint(c.UnsignedLine()))
|
commitID, err := git.LineBlameCommitID(ctx, repo, branch, c.TreePath, uint(c.UnsignedLine()))
|
||||||
if isErrBlameNotFoundOrNotEnoughLines(err) {
|
if isErrBlameInvalidated(err) {
|
||||||
c.Invalidated = true
|
c.Invalidated = true
|
||||||
return issues_model.UpdateCommentInvalidate(ctx, c)
|
return issues_model.UpdateCommentInvalidate(ctx, c)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if c.CommitSHA != "" && c.CommitSHA != commit.ID.String() {
|
if c.CommitSHA != "" && c.CommitSHA != commitID {
|
||||||
c.Invalidated = true
|
c.Invalidated = true
|
||||||
return issues_model.UpdateCommentInvalidate(ctx, c)
|
return issues_model.UpdateCommentInvalidate(ctx, c)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InvalidateCodeComments will lookup the prs for code comments which got invalidated by change
|
// InvalidateCodeComments will look up the prs for code comments which got invalidated by change
|
||||||
func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestList, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, branch string) error {
|
func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestList, repo *repo_model.Repository, branch string) error {
|
||||||
if len(prs) == 0 {
|
if len(prs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -97,35 +82,31 @@ func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestLis
|
|||||||
return fmt.Errorf("find code comments: %v", err)
|
return fmt.Errorf("find code comments: %v", err)
|
||||||
}
|
}
|
||||||
for _, comment := range codeComments {
|
for _, comment := range codeComments {
|
||||||
if err := checkInvalidation(ctx, comment, repo, gitRepo, branch); err != nil {
|
if err := checkInvalidation(ctx, comment, repo, branch); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateCodeComment creates a comment on the code line
|
// CreateCodeComment creates a code comment in a review on the code line:
|
||||||
func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, line int64, content, treePath string, pendingReview bool, replyReviewID int64, latestCommitID string, attachments []string) (*issues_model.Comment, error) {
|
// When pendingReview=false:
|
||||||
var (
|
// * no replyReviewID: create a new review and add the comment to it, and submit the review
|
||||||
existsReview bool
|
// * replyReviewID is set: add the comment to the existing submitted review
|
||||||
err error
|
// When pendingReview=true: get current pending review or create a new one, and add the comment to it
|
||||||
)
|
func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, line int64, content, treePath string, pendingReview bool, replyReviewID int64, latestCommitID string, attachments []string) (_ *issues_model.Comment, err error) {
|
||||||
|
replyToSubmittedReview := !pendingReview && replyReviewID != 0
|
||||||
// CreateCodeComment() is used for:
|
if replyToSubmittedReview {
|
||||||
// - Single comments
|
|
||||||
// - Comments that are part of a review
|
|
||||||
// - Comments that reply to an existing review
|
|
||||||
|
|
||||||
if !pendingReview && replyReviewID != 0 {
|
|
||||||
// It's not part of a review; maybe a reply to a review comment or a single comment.
|
// It's not part of a review; maybe a reply to a review comment or a single comment.
|
||||||
// Check if there are reviews for that line already; if there are, this is a reply
|
// Check if there are reviews for that line already; if there are, this is a reply
|
||||||
if existsReview, err = issues_model.ReviewExists(ctx, issue, treePath, line); err != nil {
|
if replyToSubmittedReview, err = issues_model.ReviewExists(ctx, issue, treePath, line); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comments that are replies don't require a review header to show up in the issue view
|
// Comments that are replies don't require a review header to show up in the issue view
|
||||||
if !pendingReview && existsReview {
|
// Add the code comment to existing submitted review
|
||||||
|
if replyToSubmittedReview {
|
||||||
if err = issue.LoadRepo(ctx); err != nil {
|
if err = issue.LoadRepo(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -154,39 +135,43 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
|
|||||||
return comment, nil
|
return comment, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
review, err := issues_model.GetCurrentReview(ctx, doer, issue)
|
// If no existing submitted review, create a pending review and its comment in a single transaction
|
||||||
if err != nil {
|
// to avoid failed code comment creation leaving a pending review without any comments
|
||||||
if !issues_model.IsErrReviewNotExist(err) {
|
comment, err := db.WithTx2(ctx, func(ctx context.Context) (*issues_model.Comment, error) {
|
||||||
|
curReview, err := issues_model.GetCurrentReview(ctx, doer, issue)
|
||||||
|
if errors.Is(err, util.ErrNotExist) {
|
||||||
|
// create a new pending review for the code comment
|
||||||
|
if curReview, err = issues_model.CreateReview(ctx, issues_model.CreateReviewOptions{
|
||||||
|
Type: issues_model.ReviewTypePending,
|
||||||
|
Reviewer: doer,
|
||||||
|
Issue: issue,
|
||||||
|
Official: false,
|
||||||
|
CommitID: latestCommitID,
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// add the code comment to the pending review
|
||||||
if review, err = issues_model.CreateReview(ctx, issues_model.CreateReviewOptions{
|
return createCodeComment(ctx,
|
||||||
Type: issues_model.ReviewTypePending,
|
doer,
|
||||||
Reviewer: doer,
|
issue.Repo,
|
||||||
Issue: issue,
|
issue,
|
||||||
Official: false,
|
content,
|
||||||
CommitID: latestCommitID,
|
treePath,
|
||||||
}); err != nil {
|
line,
|
||||||
return nil, err
|
curReview.ID,
|
||||||
}
|
attachments,
|
||||||
}
|
)
|
||||||
|
})
|
||||||
comment, err := createCodeComment(ctx,
|
|
||||||
doer,
|
|
||||||
issue.Repo,
|
|
||||||
issue,
|
|
||||||
content,
|
|
||||||
treePath,
|
|
||||||
line,
|
|
||||||
review.ID,
|
|
||||||
attachments,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !pendingReview && !existsReview {
|
if !pendingReview {
|
||||||
// Submit the review we've just created so the comment shows up in the issue view
|
// Doer is creating a submitted review, so submit the pending review we've just created,
|
||||||
|
// then the comment shows up in the issue view
|
||||||
if _, _, err = SubmitReview(ctx, doer, gitRepo, issue, issues_model.ReviewTypeComment, "", latestCommitID, nil); err != nil {
|
if _, _, err = SubmitReview(ctx, doer, gitRepo, issue, issues_model.ReviewTypeComment, "", latestCommitID, nil); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -214,7 +199,7 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo
|
|||||||
defer closer.Close()
|
defer closer.Close()
|
||||||
|
|
||||||
invalidated := false
|
invalidated := false
|
||||||
head := pr.GetGitHeadRefName()
|
headRef := pr.GetGitHeadRefName()
|
||||||
if line > 0 {
|
if line > 0 {
|
||||||
if reviewID != 0 {
|
if reviewID != 0 {
|
||||||
first, err := issues_model.FindComments(ctx, &issues_model.FindCommentsOptions{
|
first, err := issues_model.FindComments(ctx, &issues_model.FindCommentsOptions{
|
||||||
@@ -227,62 +212,55 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo
|
|||||||
Page: 1,
|
Page: 1,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err == nil && len(first) > 0 {
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("find first comment for %d line %d path %s. Error: %w", reviewID, line, treePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(first) > 0 {
|
||||||
|
// if there are existing comments, use the first comment's information to create the new comment
|
||||||
commitID = first[0].CommitSHA
|
commitID = first[0].CommitSHA
|
||||||
invalidated = first[0].Invalidated
|
invalidated = first[0].Invalidated
|
||||||
patch = first[0].Patch
|
patch = first[0].Patch
|
||||||
} else if err != nil && !issues_model.IsErrCommentNotExist(err) {
|
|
||||||
return nil, fmt.Errorf("Find first comment for %d line %d path %s. Error: %w", reviewID, line, treePath, err)
|
|
||||||
} else {
|
} else {
|
||||||
|
// otherwise, try to get the commitID from the review itself
|
||||||
review, err := issues_model.GetReviewByID(ctx, reviewID)
|
review, err := issues_model.GetReviewByID(ctx, reviewID)
|
||||||
if err == nil && len(review.CommitID) > 0 {
|
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||||
head = review.CommitID
|
return nil, fmt.Errorf("createCodeComment: get review %d: %w", reviewID, err)
|
||||||
} else if err != nil && !issues_model.IsErrReviewNotExist(err) {
|
}
|
||||||
return nil, fmt.Errorf("GetReviewByID %d. Error: %w", reviewID, err)
|
if review != nil && review.CommitID != "" {
|
||||||
|
headRef = review.CommitID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(commitID) == 0 {
|
if commitID == "" {
|
||||||
// FIXME validate treePath
|
// FIXME validate treePath (TODO: unclear comment)
|
||||||
// Get latest commit referencing the commented line
|
// Get latest commit referencing the commented line, error can be ignored then the head commit will be used as fallback
|
||||||
// No need for get commit for base branch changes
|
commitID, err = git.LineBlameCommitID(ctx, pr.BaseRepo, headRef, treePath, uint(line))
|
||||||
commit, err := lineBlame(ctx, pr.BaseRepo, gitRepo, head, treePath, uint(line))
|
// Preserve attribution to the reviewed revision, falling back to the PR head if blame fails.
|
||||||
if err == nil {
|
if err != nil && headRef != pr.GetGitHeadRefName() {
|
||||||
commitID = commit.ID.String()
|
commitID, err = git.LineBlameCommitID(ctx, pr.BaseRepo, pr.GetGitHeadRefName(), treePath, uint(line))
|
||||||
} else if !isErrBlameNotFoundOrNotEnoughLines(err) {
|
}
|
||||||
return nil, fmt.Errorf("LineBlame[%s, %s, %s, %d]: %w", pr.GetGitHeadRefName(), gitRepo.LogString(), treePath, line, err)
|
if err != nil {
|
||||||
|
log.Debug("LineBlame[%s, %s, %s, %d]: %v", pr.GetGitHeadRefName(), gitRepo.LogString(), treePath, line, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only fetch diff if comment is review comment
|
// Only fetch diff if comment is review comment
|
||||||
if len(patch) == 0 && reviewID != 0 {
|
if patch == "" && reviewID != 0 {
|
||||||
headCommitID, err := gitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName())
|
headCommitID, err := gitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GetRefCommitID[%s]: %w", pr.GetGitHeadRefName(), err)
|
return nil, fmt.Errorf("GetRefCommitID[%s]: %w", pr.GetGitHeadRefName(), err)
|
||||||
}
|
}
|
||||||
if len(commitID) == 0 {
|
if commitID == "" {
|
||||||
commitID = headCommitID
|
commitID = headCommitID
|
||||||
}
|
}
|
||||||
|
|
||||||
patch, err = git.GetFileDiffCutAroundLine(ctx,
|
patch, err = gitdiff.GetCodeCommentPatch(ctx, gitRepo, pr.MergeBase, headCommitID, treePath, line)
|
||||||
gitRepo, pr.MergeBase, headCommitID, treePath,
|
|
||||||
int64((&issues_model.Comment{Line: line}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If patch is still empty (unchanged line), generate code context
|
|
||||||
contextCommitID := util.Iif(line < 0, pr.MergeBase, headCommitID) // the line number is relative to the diff sides, not to the blamed commitID
|
|
||||||
if patch == "" && contextCommitID != "" {
|
|
||||||
patch, err = gitdiff.GeneratePatchForUnchangedLine(ctx, gitRepo, contextCommitID, treePath, line, setting.UI.CodeCommentLines)
|
|
||||||
if err != nil {
|
|
||||||
// Log the error but don't fail comment creation
|
|
||||||
log.Debug("Unable to generate patch for unchanged line (file=%s, line=%d, commit=%s): %v", treePath, line, contextCommitID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
|
return issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
|
||||||
Type: issues_model.CommentTypeCode,
|
Type: issues_model.CommentTypeCode,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"gitea.dev/models/unittest"
|
"gitea.dev/models/unittest"
|
||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
"gitea.dev/modules/git"
|
"gitea.dev/modules/git"
|
||||||
|
"gitea.dev/modules/util"
|
||||||
pull_service "gitea.dev/services/pull"
|
pull_service "gitea.dev/services/pull"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -53,7 +54,6 @@ func TestDismissReview(t *testing.T) {
|
|||||||
func TestCreateCodeCommentContextOfUnchangedLine(t *testing.T) {
|
func TestCreateCodeCommentContextOfUnchangedLine(t *testing.T) {
|
||||||
require.NoError(t, unittest.PrepareTestDatabase())
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
ctx := t.Context()
|
ctx := t.Context()
|
||||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
||||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 6})
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 6})
|
||||||
require.NoError(t, pr.LoadBaseRepo(ctx))
|
require.NoError(t, pr.LoadBaseRepo(ctx))
|
||||||
|
|
||||||
@@ -86,22 +86,76 @@ func TestCreateCodeCommentContextOfUnchangedLine(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
mergeBase, err := headCommit.ParentID(0)
|
mergeBase, err := headCommit.ParentID(0)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
mergeBaseCommit, err := gitRepo.GetCommit(ctx, mergeBase.String())
|
||||||
|
require.NoError(t, err)
|
||||||
|
lineCommitID, err := mergeBaseCommit.ParentID(0)
|
||||||
|
require.NoError(t, err)
|
||||||
pr.MergeBase = mergeBase.String()
|
pr.MergeBase = mergeBase.String()
|
||||||
require.NoError(t, pr.UpdateCols(ctx, "merge_base"))
|
require.NoError(t, pr.UpdateCols(ctx, "merge_base"))
|
||||||
|
|
||||||
for _, c := range []struct {
|
for _, c := range []struct {
|
||||||
name string
|
name string
|
||||||
line int64
|
line int64
|
||||||
|
doerID int64
|
||||||
|
commitID string
|
||||||
}{
|
}{
|
||||||
{"NewSide", 17},
|
{"NewSide", 17, 2, ""},
|
||||||
{"OldSide", -15},
|
{"OldSide", -15, 2, ""},
|
||||||
|
{"UnknownReviewCommit", 17, 4, strings.Repeat("a", 40)},
|
||||||
} {
|
} {
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: c.doerID})
|
||||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: pr.IssueID})
|
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: pr.IssueID})
|
||||||
require.NoError(t, issue.LoadRepo(ctx))
|
require.NoError(t, issue.LoadRepo(ctx))
|
||||||
comment, err := pull_service.CreateCodeComment(ctx, doer, gitRepo, issue, c.line, "test", treePath, true, 0, headCommit.ID.String(), nil)
|
comment, err := pull_service.CreateCodeComment(ctx, doer, gitRepo, issue, c.line, "test", treePath, true, 0, c.commitID, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, strings.HasSuffix(comment.Patch, "\n old 10\n"), "patch: %q", comment.Patch)
|
assert.True(t, strings.HasSuffix(comment.Patch, "\n old 10\n"), "patch: %q", comment.Patch)
|
||||||
|
if c.line > 0 {
|
||||||
|
assert.Equal(t, lineCommitID.String(), comment.CommitSHA)
|
||||||
|
require.NoError(t, pull_service.InvalidateCodeComments(ctx, issues_model.PullRequestList{pr}, pr.BaseRepo, headRef))
|
||||||
|
comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: comment.ID})
|
||||||
|
assert.False(t, comment.Invalidated)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateCodeCommentUnknownCommitID(t *testing.T) {
|
||||||
|
unittest.PrepareTestEnv(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
|
||||||
|
// org3/repo3.git: "test_branch" exists, but "refs/pull/2/head" (PR ID: 6) doesn't exist yet
|
||||||
|
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 6})
|
||||||
|
require.NoError(t, pr.LoadBaseRepo(ctx))
|
||||||
|
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: pr.IssueID})
|
||||||
|
require.NoError(t, issue.LoadRepo(ctx))
|
||||||
|
|
||||||
|
gitRepo, err := git.OpenRepository(ctx, pr.BaseRepo)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer gitRepo.Close()
|
||||||
|
|
||||||
|
const treePath = "file.txt"
|
||||||
|
|
||||||
|
t.Run("RollbackOnFailure", func(t *testing.T) {
|
||||||
|
// the PR head ref doesn't exist yet, so creating the comment fails
|
||||||
|
_, err := gitRepo.GetCommit(ctx, pr.GetGitHeadRefName())
|
||||||
|
require.ErrorIs(t, err, util.ErrNotExist)
|
||||||
|
_, err = pull_service.CreateCodeComment(ctx, doer, gitRepo, issue, 1, "test", treePath, true, 0, "", nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
_, err = issues_model.GetCurrentReview(ctx, doer, issue)
|
||||||
|
assert.True(t, issues_model.IsErrReviewNotExist(err), "got: %v", err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("FallbackToHead", func(t *testing.T) {
|
||||||
|
headCommitID, err := gitRepo.GetBranchCommitID(ctx, pr.HeadBranch)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, git.UpdateRef(ctx, gitRepo, pr.GetGitHeadRefName(), headCommitID))
|
||||||
|
// the first comment creates the pending review with the unknown commit, the second one reuses it
|
||||||
|
for i, commitID := range []string{strings.Repeat("a", 40), headCommitID} {
|
||||||
|
comment, err := pull_service.CreateCodeComment(ctx, doer, gitRepo, issue, int64(i+1), "test", treePath, true, 0, commitID, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, headCommitID, comment.CommitSHA)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package release
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
issues_model "gitea.dev/models/issues"
|
||||||
|
repo_model "gitea.dev/models/repo"
|
||||||
|
user_model "gitea.dev/models/user"
|
||||||
|
"gitea.dev/modules/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AllowedReleaseReactions() []string {
|
||||||
|
return setting.UI.ReleaseReactions
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateReleaseReaction(ctx context.Context, doer *user_model.User, rel *repo_model.Release, content string) (*issues_model.Reaction, error) {
|
||||||
|
if err := rel.LoadRepo(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if user_model.IsUserBlockedBy(ctx, doer, rel.PublisherID, rel.Repo.OwnerID) {
|
||||||
|
return nil, user_model.ErrBlockedUser
|
||||||
|
}
|
||||||
|
|
||||||
|
return issues_model.CreateReaction(ctx, setting.UI.ReleaseReactionsLookup, &issues_model.ReactionOptions{
|
||||||
|
Type: content,
|
||||||
|
DoerID: doer.ID,
|
||||||
|
ReleaseID: rel.ID,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"gitea.dev/models/db"
|
"gitea.dev/models/db"
|
||||||
git_model "gitea.dev/models/git"
|
git_model "gitea.dev/models/git"
|
||||||
|
issues_model "gitea.dev/models/issues"
|
||||||
repo_model "gitea.dev/models/repo"
|
repo_model "gitea.dev/models/repo"
|
||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
"gitea.dev/modules/container"
|
"gitea.dev/modules/container"
|
||||||
@@ -421,6 +422,10 @@ func DeleteReleaseByID(ctx context.Context, repo *repo_model.Repository, rel *re
|
|||||||
return fmt.Errorf("DeleteAttachments: %w", err)
|
return fmt.Errorf("DeleteAttachments: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err := db.DeleteByBean(ctx, &issues_model.Reaction{ReleaseID: rel.ID}); err != nil {
|
||||||
|
return fmt.Errorf("DeleteReleaseByID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
for i := range rel.Attachments {
|
for i := range rel.Attachments {
|
||||||
attachment := rel.Attachments[i]
|
attachment := rel.Attachments[i]
|
||||||
if err := storage.Attachments.Delete(attachment.RelativePath()); err != nil {
|
if err := storage.Attachments.Delete(attachment.RelativePath()); err != nil {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.dev/models/db"
|
||||||
|
issues_model "gitea.dev/models/issues"
|
||||||
repo_model "gitea.dev/models/repo"
|
repo_model "gitea.dev/models/repo"
|
||||||
"gitea.dev/models/unittest"
|
"gitea.dev/models/unittest"
|
||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
@@ -407,6 +409,25 @@ func TestCreateNewTag(t *testing.T) {
|
|||||||
"v2.0 is released \n\n BUGFIX: .... \n\n 123"))
|
"v2.0 is released \n\n BUGFIX: .... \n\n 123"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateReleaseReaction(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: 1})
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"+1", "laugh", "hooray", "heart", "rocket", "eyes"}, AllowedReleaseReactions())
|
||||||
|
|
||||||
|
_, err := CreateReleaseReaction(t.Context(), user, release, "heart")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NoError(t, DeleteReleaseByID(t.Context(), repo, release, user, false))
|
||||||
|
unittest.AssertNotExistsBean(t, &issues_model.Reaction{ReleaseID: release.ID})
|
||||||
|
|
||||||
|
assert.NoError(t, db.Insert(t.Context(), &user_model.Blocking{BlockerID: repo.OwnerID, BlockeeID: user.ID}))
|
||||||
|
_, err = CreateReleaseReaction(t.Context(), user, release, "heart")
|
||||||
|
assert.ErrorIs(t, err, user_model.ErrBlockedUser)
|
||||||
|
}
|
||||||
|
|
||||||
func TestRelease_DatedByTargetCommit(t *testing.T) {
|
func TestRelease_DatedByTargetCommit(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
|||||||
@@ -139,6 +139,11 @@ func DeleteRepositoryDirectly(ctx context.Context, repoID int64, ignoreOrgTeams
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err := db.GetEngine(ctx).In("release_id", builder.Select("id").From("`release`").Where(builder.Eq{"repo_id": repoID})).
|
||||||
|
Delete(&issues_model.Reaction{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// CleanupEphemeralRunnersByPickedTaskOfRepo deletes ephemeral global/org/user that have started any task of this repo
|
// CleanupEphemeralRunnersByPickedTaskOfRepo deletes ephemeral global/org/user that have started any task of this repo
|
||||||
// The cannot pick a second task hardening for ephemeral runners expect that task objects remain available until runner deletion
|
// The cannot pick a second task hardening for ephemeral runners expect that task objects remain available until runner deletion
|
||||||
// This method will delete affected ephemeral global/org/user runners
|
// This method will delete affected ephemeral global/org/user runners
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
actions_model "gitea.dev/models/actions"
|
actions_model "gitea.dev/models/actions"
|
||||||
"gitea.dev/models/db"
|
"gitea.dev/models/db"
|
||||||
git_model "gitea.dev/models/git"
|
git_model "gitea.dev/models/git"
|
||||||
|
issues_model "gitea.dev/models/issues"
|
||||||
"gitea.dev/models/organization"
|
"gitea.dev/models/organization"
|
||||||
repo_model "gitea.dev/models/repo"
|
repo_model "gitea.dev/models/repo"
|
||||||
"gitea.dev/models/unittest"
|
"gitea.dev/models/unittest"
|
||||||
@@ -75,6 +76,7 @@ func TestDeleteRepositoryDirectlyPurgesRepoScopedRows(t *testing.T) {
|
|||||||
&git_model.RenamedBranch{RepoID: 1, From: "old-name", To: "new-name"},
|
&git_model.RenamedBranch{RepoID: 1, From: "old-name", To: "new-name"},
|
||||||
&git_model.CommitStatusSummary{RepoID: 1, SHA: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", State: "success"},
|
&git_model.CommitStatusSummary{RepoID: 1, SHA: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", State: "success"},
|
||||||
&repo_model.RepoTransfer{RepoID: 1, DoerID: 2, RecipientID: 3},
|
&repo_model.RepoTransfer{RepoID: 1, DoerID: 2, RecipientID: 3},
|
||||||
|
&issues_model.Reaction{Type: "heart", UserID: 2, ReleaseID: 1},
|
||||||
))
|
))
|
||||||
unittest.AssertExistsAndLoadBean(t, &git_model.CommitStatusIndex{RepoID: 1})
|
unittest.AssertExistsAndLoadBean(t, &git_model.CommitStatusIndex{RepoID: 1})
|
||||||
|
|
||||||
@@ -87,4 +89,5 @@ func TestDeleteRepositoryDirectlyPurgesRepoScopedRows(t *testing.T) {
|
|||||||
unittest.AssertNotExistsBean(t, &git_model.CommitStatusSummary{RepoID: 1})
|
unittest.AssertNotExistsBean(t, &git_model.CommitStatusSummary{RepoID: 1})
|
||||||
unittest.AssertNotExistsBean(t, &git_model.CommitStatusIndex{RepoID: 1})
|
unittest.AssertNotExistsBean(t, &git_model.CommitStatusIndex{RepoID: 1})
|
||||||
unittest.AssertNotExistsBean(t, &repo_model.RepoTransfer{RepoID: 1})
|
unittest.AssertNotExistsBean(t, &repo_model.RepoTransfer{RepoID: 1})
|
||||||
|
unittest.AssertNotExistsBean(t, &issues_model.Reaction{ReleaseID: 1})
|
||||||
}
|
}
|
||||||
@@ -129,7 +129,7 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete Reactions
|
// Delete Reactions
|
||||||
if err = issues_model.DeleteReaction(ctx, &issues_model.ReactionOptions{DoerID: u.ID}); err != nil {
|
if err = issues_model.DeleteUserReactions(ctx, u.ID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
"gitea.dev/models/unit"
|
"gitea.dev/models/unit"
|
||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
"gitea.dev/modules/git"
|
"gitea.dev/modules/git"
|
||||||
"gitea.dev/modules/git/gitcmd"
|
|
||||||
"gitea.dev/modules/globallock"
|
"gitea.dev/modules/globallock"
|
||||||
"gitea.dev/modules/graceful"
|
"gitea.dev/modules/graceful"
|
||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
@@ -50,17 +49,18 @@ func InitWiki(ctx context.Context, repo *repo_model.Repository) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// prepareGitPath try to find a suitable file path with file name by the given raw wiki name.
|
// prepareGitPath try to find a suitable file path with file name by the given raw wiki name.
|
||||||
|
// An empty defaultWikiBranch means the branch doesn't exist yet.
|
||||||
// return: existence, prepared file path with name, error
|
// return: existence, prepared file path with name, error
|
||||||
func prepareGitPath(ctx context.Context, gitRepo *git.Repository, defaultWikiBranch string, wikiPath WebPath) (bool, string, error) {
|
func prepareGitPath(ctx context.Context, gitRepo *git.Repository, defaultWikiBranch string, wikiPath WebPath) (bool, string, error) {
|
||||||
unescaped := string(wikiPath) + ".md"
|
unescaped := string(wikiPath) + ".md"
|
||||||
gitPath := WebPathToGitPath(wikiPath)
|
gitPath := WebPathToGitPath(wikiPath)
|
||||||
|
if defaultWikiBranch == "" {
|
||||||
|
return false, gitPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Look for both files
|
// Look for both files
|
||||||
filesInIndex, err := gitRepo.LsTree(ctx, defaultWikiBranch, unescaped, gitPath)
|
filesInIndex, err := gitRepo.LsTree(ctx, defaultWikiBranch, unescaped, gitPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if gitcmd.IsStderr(err, gitcmd.StderrNotValidObjectName) {
|
|
||||||
return false, gitPath, nil // branch doesn't exist
|
|
||||||
}
|
|
||||||
log.Error("Wiki LsTree failed, err: %v", err)
|
log.Error("Wiki LsTree failed, err: %v", err)
|
||||||
return false, gitPath, err
|
return false, gitPath, err
|
||||||
}
|
}
|
||||||
@@ -136,7 +136,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isWikiExist, newWikiPath, err := prepareGitPath(ctx, gitRepo, repo.DefaultWikiBranch, newWikiName)
|
isWikiExist, newWikiPath, err := prepareGitPath(ctx, gitRepo, cloneOpts.Branch, newWikiName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -152,7 +152,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
|
|||||||
isOldWikiExist := true
|
isOldWikiExist := true
|
||||||
oldWikiPath := newWikiPath
|
oldWikiPath := newWikiPath
|
||||||
if oldWikiName != newWikiName {
|
if oldWikiName != newWikiName {
|
||||||
isOldWikiExist, oldWikiPath, err = prepareGitPath(ctx, gitRepo, repo.DefaultWikiBranch, oldWikiName)
|
isOldWikiExist, oldWikiPath, err = prepareGitPath(ctx, gitRepo, cloneOpts.Branch, oldWikiName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -300,18 +300,7 @@ func TestPrepareWikiFileName(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPrepareWikiFileName_FirstPage(t *testing.T) {
|
func TestPrepareWikiFileName_FirstPage(t *testing.T) {
|
||||||
// Now create a temporaryDirectory
|
existence, newWikiPath, err := prepareGitPath(t.Context(), nil, "", "Home")
|
||||||
tmpDir := t.TempDir()
|
|
||||||
|
|
||||||
err := git.InitRepositoryLocal(t.Context(), tmpDir, true, git.Sha1ObjectFormat.Name(), "")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
gitRepo, err := git.OpenRepositoryLocal(t.Context(), tmpDir)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
defer gitRepo.Close()
|
|
||||||
|
|
||||||
existence, newWikiPath, err := prepareGitPath(t.Context(), gitRepo, "master", "Home")
|
|
||||||
assert.False(t, existence)
|
assert.False(t, existence)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "Home.md", newWikiPath)
|
assert.Equal(t, "Home.md", newWikiPath)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ui g-modal-confirm modal" id="delete-badge-modal">
|
<dialog class="modal-dialog" id="delete-badge-modal">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
{{svg "octicon-trash"}}
|
{{svg "octicon-trash"}}
|
||||||
{{ctx.Locale.Tr "admin.badges.delete_badge"}}
|
{{ctx.Locale.Tr "admin.badges.delete_badge"}}
|
||||||
@@ -39,6 +39,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{{template "base/modal_actions_confirm" .}}
|
{{template "base/modal_actions_confirm" .}}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</dialog>
|
||||||
|
|
||||||
{{template "admin/layout_footer" .}}
|
{{template "admin/layout_footer" .}}
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
|
|
||||||
{{template "base/paginate" .}}
|
{{template "base/paginate" .}}
|
||||||
|
|
||||||
<div class="ui g-modal-confirm modal" id="change-email-modal">
|
<dialog class="modal-dialog" id="change-email-modal">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
{{ctx.Locale.Tr "admin.emails.change_email_header"}}
|
{{ctx.Locale.Tr "admin.emails.change_email_header"}}
|
||||||
</div>
|
</div>
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
|
|
||||||
{{template "base/modal_actions_confirm" .}}
|
{{template "base/modal_actions_confirm" .}}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{template "admin/layout_footer" .}}
|
{{template "admin/layout_footer" .}}
|
||||||
@@ -61,9 +61,9 @@
|
|||||||
{{template "base/paginate" .}}
|
{{template "base/paginate" .}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ui modal admin" id="detail-modal">
|
<dialog class="modal-dialog admin" id="detail-modal">
|
||||||
<div class="header">{{ctx.Locale.Tr "admin.notices.view_detail_header"}}</div>
|
<div class="header">{{ctx.Locale.Tr "admin.notices.view_detail_header"}}</div>
|
||||||
<div class="content"><pre></pre></div>
|
<div class="content"><pre></pre></div>
|
||||||
</div>
|
</dialog>
|
||||||
|
|
||||||
{{template "admin/layout_footer" .}}
|
{{template "admin/layout_footer" .}}
|
||||||
@@ -83,12 +83,14 @@
|
|||||||
{{template "base/paginate" .}}
|
{{template "base/paginate" .}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="ui small modal form-fetch-action" method="post" id="admin-package-delete-modal">
|
<dialog class="modal-dialog" id="admin-package-delete-modal">
|
||||||
<div class="header">{{svg "octicon-trash"}} {{ctx.Locale.Tr "packages.settings.delete.version"}}</div>
|
<div class="header">{{svg "octicon-trash"}} {{ctx.Locale.Tr "packages.settings.delete.version"}}</div>
|
||||||
<div class="content">
|
<form class="form-fetch-action" method="post">
|
||||||
{{ctx.Locale.Tr "packages.settings.delete.notice" (HTMLFormat `<span class="%s"></span>` "package-name") (HTMLFormat `<span class="%s"></span>` "package-version")}}
|
<div class="content">
|
||||||
</div>
|
{{ctx.Locale.Tr "packages.settings.delete.notice" (HTMLFormat `<span class="%s"></span>` "package-name") (HTMLFormat `<span class="%s"></span>` "package-version")}}
|
||||||
{{template "base/modal_actions_confirm" .}}
|
</div>
|
||||||
</form>
|
{{template "base/modal_actions_confirm" .}}
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
{{template "admin/layout_footer" .}}
|
{{template "admin/layout_footer" .}}
|
||||||
@@ -96,14 +96,16 @@
|
|||||||
{{template "base/paginate" .}}
|
{{template "base/paginate" .}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="ui small modal form-fetch-action" id="admin-repo-delete-modal" method="post">
|
<dialog class="modal-dialog" id="admin-repo-delete-modal">
|
||||||
<div class="header">{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.settings.delete"}}</div>
|
<div class="header">{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.settings.delete"}}</div>
|
||||||
<div class="content">
|
<form class="form-fetch-action" method="post">
|
||||||
<p>{{ctx.Locale.Tr "repo.settings.delete_desc"}}</p>
|
<div class="content">
|
||||||
{{ctx.Locale.Tr "repo.settings.delete_notices_2" (HTMLFormat `<span class="%s"></span>` "repo-name")}}<br>
|
<p>{{ctx.Locale.Tr "repo.settings.delete_desc"}}</p>
|
||||||
{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}
|
{{ctx.Locale.Tr "repo.settings.delete_notices_2" (HTMLFormat `<span class="%s"></span>` "repo-name")}}<br>
|
||||||
</div>
|
{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}
|
||||||
{{template "base/modal_actions_confirm" .}}
|
</div>
|
||||||
</form>
|
{{template "base/modal_actions_confirm" .}}
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
{{template "admin/layout_footer" .}}
|
{{template "admin/layout_footer" .}}
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<span class="tw-flex-1"> {{svg "octicon-file-directory-fill"}} {{$dir}}</span>
|
<span class="tw-flex-1"> {{svg "octicon-file-directory-fill"}} {{$dir}}</span>
|
||||||
<div>
|
<div>
|
||||||
<button type="button" class="ui button primary show-modal tw-p-2" data-modal="#adopt-unadopted-modal-{{$dirI}}">{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</button>
|
<button type="button" class="ui button primary show-modal tw-p-2" data-modal="#adopt-unadopted-modal-{{$dirI}}">{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</button>
|
||||||
<div class="ui g-modal-confirm modal" id="adopt-unadopted-modal-{{$dirI}}">
|
<dialog class="modal-dialog" id="adopt-unadopted-modal-{{$dirI}}">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting"}}</span>
|
<span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting"}}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,9 +38,9 @@
|
|||||||
<input type="hidden" name="page" value="{{$.CurrentPage}}">
|
<input type="hidden" name="page" value="{{$.CurrentPage}}">
|
||||||
{{template "base/modal_actions_confirm"}}
|
{{template "base/modal_actions_confirm"}}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</dialog>
|
||||||
<button type="button" class="ui button red show-modal tw-p-2" data-modal="#delete-unadopted-modal-{{$dirI}}">{{svg "octicon-x"}} {{ctx.Locale.Tr "repo.delete_preexisting_label"}}</button>
|
<button type="button" class="ui button red show-modal tw-p-2" data-modal="#delete-unadopted-modal-{{$dirI}}">{{svg "octicon-x"}} {{ctx.Locale.Tr "repo.delete_preexisting_label"}}</button>
|
||||||
<div class="ui g-modal-confirm modal" id="delete-unadopted-modal-{{$dirI}}">
|
<dialog class="modal-dialog" id="delete-unadopted-modal-{{$dirI}}">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<span class="label">{{ctx.Locale.Tr "repo.delete_preexisting"}}</span>
|
<span class="label">{{ctx.Locale.Tr "repo.delete_preexisting"}}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
<input type="hidden" name="page" value="{{$.CurrentPage}}">
|
<input type="hidden" name="page" value="{{$.CurrentPage}}">
|
||||||
{{template "base/modal_actions_confirm"}}
|
{{template "base/modal_actions_confirm"}}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</dialog>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -223,7 +223,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ui g-modal-confirm delete modal" id="delete-user-modal">
|
<dialog class="modal-dialog" id="delete-user-modal">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
{{svg "octicon-trash"}}
|
{{svg "octicon-trash"}}
|
||||||
{{ctx.Locale.Tr "settings.delete_account_title"}}
|
{{ctx.Locale.Tr "settings.delete_account_title"}}
|
||||||
@@ -241,6 +241,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{{template "base/modal_actions_confirm" .}}
|
{{template "base/modal_actions_confirm" .}}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</dialog>
|
||||||
|
|
||||||
{{template "admin/layout_footer" .}}
|
{{template "admin/layout_footer" .}}
|
||||||
@@ -5,18 +5,20 @@
|
|||||||
<span>{{ctx.Locale.Tr "settings.organization"}} ({{ctx.Locale.Tr "admin.total" $userOrgsTotal}})</span>
|
<span>{{ctx.Locale.Tr "settings.organization"}} ({{ctx.Locale.Tr "admin.total" $userOrgsTotal}})</span>
|
||||||
{{if $userOrgs}}
|
{{if $userOrgs}}
|
||||||
<button type="button" class="ui red tiny button show-modal" data-modal="#remove-all-orgs-modal">{{ctx.Locale.Tr "remove_all"}}</button>
|
<button type="button" class="ui red tiny button show-modal" data-modal="#remove-all-orgs-modal">{{ctx.Locale.Tr "remove_all"}}</button>
|
||||||
<form class="ui small modal form-fetch-action" id="remove-all-orgs-modal" method="post" action="{{ctx.RootData.Link}}/orgs/remove-all">
|
<dialog class="modal-dialog" id="remove-all-orgs-modal">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
{{ctx.Locale.Tr "admin.users.remove_all_orgs_title"}}
|
{{ctx.Locale.Tr "admin.users.remove_all_orgs_title"}}
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<form class="form-fetch-action" method="post" action="{{ctx.RootData.Link}}/orgs/remove-all">
|
||||||
<p>{{ctx.Locale.Tr "admin.users.remove_all_orgs_desc" $user.Name $userOrgsTotal}}</p>
|
<div class="content">
|
||||||
</div>
|
<p>{{ctx.Locale.Tr "admin.users.remove_all_orgs_desc" $user.Name $userOrgsTotal}}</p>
|
||||||
<div class="actions">
|
</div>
|
||||||
<div class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</div>
|
<div class="actions">
|
||||||
<button class="ui red button" type="submit">{{ctx.Locale.Tr "remove_all"}}</button>
|
<div class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</div>
|
||||||
</div>
|
<button class="ui red button" type="submit">{{ctx.Locale.Tr "remove_all"}}</button>
|
||||||
</form>
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
{{end}}
|
{{end}}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
|
|||||||
Loaded 100 of 210 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user