Compare commits

...
Author SHA1 Message Date
wxiaoguang 76e88b30c0 fix: use clear error message when failed to parse git version (#39898)
fix #39892
2026-10-20 23:26:56 +00:00
silverwind 82536a3ede fix: off-by-one errors in code comment patches (#39887)
Code comments next to a diff hunk could show one line too high in the
conversation view vs. in the code view that showed them on the correct
line.

- Treat the hunk end in `CutDiffAroundLine` as exclusive, so the line
after a hunk falls back to the unchanged-line context
- Don't count `\ No newline at end of file` markers as lines when
cutting the hunk
- Share the unchanged-line fallback with migrated review comments
2026-10-20 22:10:04 +00:00
Basavaraj S mandsilverwind c52e7c0cd2 fix(packages): reject newline-bearing rubygems dependency metadata (#39782)
The RubyGems compact index (the shared `/versions` file and per-package
`info/<name>` files) is line-based.
https://github.com/go-gitea/gitea/pull/38061 rejected line breaks in the
gem name, but the platform, dependency names and version requirements
from the gemspec still accepted them, letting an uploader forge index
lines for bundler clients.

The parser now rejects a line break in any of those fields. The Marshal
based endpoints are length prefixed and unaffected.

Co-authored-by: silverwind <me@silverwind.io>
2026-10-20 21:18:44 +00:00
silverwind 738ff1d65a refactor(git): replace git stderr matching with exit codes (#39866)
Matching git's stderr text is fragile because it breaks silently when
git rewords a message. Where git offers an exit code or a query with a
defined answer, use that instead, and drop matches that could never
trigger. Follow-up to the discussion in https://github.com/go-gitea/gitea/pull/39857.

- Detect a missing remote by `git remote` exit code 2
- Resolve annotated tags to their tree with cat-file `^{tree}` instead
of spawning `ls-tree` when listing tree entries
- Skip the wiki page lookup when the default branch doesn't exist yet
- Remove unreachable "no merge base" and "unknown revision" checks
2026-10-20 21:00:32 +00:00
silverwind 52eb515751 enhance: improve hover border of colored button groups (#39861)
Hovering a button in a primary or red button group showed a grey frame,
because the generic group hover border applied to colored groups too.
Green groups had no colors at all.

Colored groups now let the hover background fill the border, and green
groups get the same colors as primary and red. The devtest page shows
all three.

<img width="569" height="304" alt="Hovered buttons in uncolored,
primary, red and green button groups, before and after"
src="https://github.com/user-attachments/assets/e0fb6b16-f1f1-459b-9848-9386cd9453c9"
/>

Extracted from https://github.com/go-gitea/gitea/pull/38404
2026-10-20 19:29:38 +00:00
wxiaoguang d7d658a8e9 fix: prevent system users from watching a repository (#39886)
fix #39884
2026-10-20 19:07:20 +00:00
Shivansh Garg 5265238726 fix(api): skip label lookup in ListIssues when no labels filter given (#39890)
strings.Split("", ",") returns []string{""} (length 1), not an empty
slice, so the previous len(splitted) > 0 guard was always true and never
skipped the label ID lookup, even when the labels query param was
absent. Check the raw trimmed param for emptiness before splitting,
mirroring the pattern already used elsewhere in this file.

Fixes: https://github.com/go-gitea/gitea/issues/39889
2026-10-20 18:46:14 +00:00
fixfonandwxiaoguang 35fa0400e9 fix(pulls): tolerate invalid stored review commit_id (#39849)
Fixes #39848

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-10-20 11:03:34 -07:00
edd0c0e2ce feat: add release reactions and a GitHub-like reaction bar (#38415)
Adds emoji reactions to releases and moves the reaction picker of
issues, comments and releases into a GitHub-like reaction bar.

- Releases omit 👎 or 😕
- Drafts and tags can't be reacted to
- Moved menu to tippy
- Added keyboard navigation

Fixes: https://github.com/go-gitea/gitea/issues/34268
Fixes: https://github.com/go-gitea/gitea/issues/16676
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-10-20 15:04:10 +02:00
silverwindandwxiaoguang e645afbb3f refactor: replace Fomantic modal with native dialog (#39846)
Replace Fomantic's hard-forked jQuery modal with native `<dialog>`
elements, advancing https://github.com/go-gitea/gitea/issues/29849. The
UI is visually unchanged. Dialogs open with `show()` over an `inert`
page, so tooltips, popups and toasts mounted on `<body>` stay usable
above them, unlike with `showModal()`
(https://github.com/whatwg/html/issues/9936).

- Remove the Fomantic modal and dimmer, add `showModal` and `hideModal`
- Declare modals as `<dialog class="modal-dialog">`, shown inside a
`.modal-backdrop`, instead of `ui modal` with its unused size classes
- Remove two modals that could never be opened
- Block the page while a modal is open, close the topmost one on Escape
and restore focus to its opener
- Allow stacked modals, use a confirm modal instead of `window.confirm`
for deleting content history
- Hide popups and toasts when a modal opens or closes
- Keep the release notes modal open when generating notes fails

Fixes: https://github.com/go-gitea/gitea/issues/39643
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-10-20 12:32:27 +02:00
Giteabot 7610336d74 chore(deps): update go toolchain directive to v1.27.3 (#39881) 2026-10-20 05:30:28 +02:00
Danny Wardandwxiaoguang 3701fcec84 fix(issues): use unified user display name pattern for more places (#39852)
* Fix #39761

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-10-20 01:13:17 +00:00
GiteaBot f8dd2dd4cc [skip ci] Updated translations via Crowdin 2026-10-20 00:51:42 +00:00
bircniandsilverwind ba4f8fa246 enhance(pull)!: add per-repository default squash merge commit message (#39816)
Adds a repository setting for the default squash merge commit message
body with GitHub's options `PR_BODY`, `COMMIT_MESSAGES` and `BLANK`,
also settable via `squash_merge_commit_message` in the API. The commit
title stays the pull request title.

1. Breaking: API squash merges without `merge_message_field` now get the
same default message as the web merge box instead of a title-only
commit, which affects clients sending only `Do` like Renovate. An empty
value keeps title-only.
2. Breaking: `[repository.pull-request] DEFAULT_SQUASH_MERGE_MESSAGE`
replaces `POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES`, which still
works as a deprecated fallback. Repositories store the default when
created or next saved.
3. Deprecated app.ini keys now migrate through one table instead of
per-loader fallbacks, which also fixes the ignored `[mailer] CERT_FILE`
and `KEY_FILE` fallbacks. Deprecation warnings no longer claim a removal
version.

<img width="1891" height="571" alt="grafik"
src="https://github.com/user-attachments/assets/0fdcde13-1f11-4156-b598-d80eb0d04cbe"
/>

---------

Co-authored-by: silverwind <me@silverwind.io>
2026-10-19 09:15:41 +00:00
210 changed files with 1819 additions and 2499 deletions

No files matched your search

+7 -3
View File
@@ -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
;; ;;
+1 -1
View File
@@ -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
+1
View File
@@ -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
} }
+33
View File
@@ -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))
}
+2 -5
View File
@@ -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
+3 -5
View File
@@ -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
View File
@@ -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
} }
+4 -6
View File
@@ -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
+13
View File
@@ -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)
+2
View File
@@ -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
} }
+3
View File
@@ -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
} }
+10 -4
View File
@@ -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})
+9
View File
@@ -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)
+6 -11
View File
@@ -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
View File
@@ -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
+12
View File
@@ -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
View File
@@ -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
View File
@@ -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
+9 -1
View File
@@ -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
View File
@@ -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 {
+2 -5
View File
@@ -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) {
-2
View File
@@ -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"},
} }
+2 -2
View File
@@ -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,
+9 -18
View File
@@ -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
} }
-5
View File
@@ -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
View File
@@ -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
+19
View File
@@ -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)
+17
View File
@@ -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) {
+64
View File
@@ -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)
}
}
}
+2 -2
View File
@@ -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)
} }
} }
-10
View File
@@ -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())
} }
+1
View File
@@ -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"))
-12
View File
@@ -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 {
+2
View File
@@ -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)
+6 -6
View File
@@ -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
} }
+2 -48
View File
@@ -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")
} }
+3 -3
View File
@@ -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
-9
View File
@@ -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)
} }
-6
View File
@@ -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)
} }
+8 -3
View File
@@ -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")
} }
-4
View File
@@ -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)
} }
+4 -28
View File
@@ -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()
+1 -1
View File
@@ -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")
+1
View File
@@ -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)
+3 -3
View File
@@ -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":
+6 -4
View File
@@ -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 + ":"
+1 -1
View File
@@ -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)
+3
View File
@@ -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.
-3
View File
@@ -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
}, },
+4
View File
@@ -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",
+7 -6
View File
@@ -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",
+3
View File
@@ -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",
+11
View File
@@ -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": "マージ許可リストに含むユーザー:",
+3
View File
@@ -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",
+12
View File
@@ -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": "选择的仓库",
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+12 -3
View File
@@ -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
+4
View File
@@ -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 {
-1
View File
@@ -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
View File
@@ -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
+1 -19
View File
@@ -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
-1
View File
@@ -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)
+1 -1
View File
@@ -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
View File
@@ -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 {
+1 -2
View File
@@ -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
View File
@@ -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())
}
+5
View File
@@ -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
View File
@@ -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)
+11 -1
View File
@@ -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) {
+1
View File
@@ -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{
+3
View File
@@ -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),
+14 -9
View File
@@ -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
+15
View File
@@ -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)
})
} }
+16
View File
@@ -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)
+3 -2
View File
@@ -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,
+3 -8
View File
@@ -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)
+2 -4
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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)
+1
View File
@@ -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
View File
@@ -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,
+60 -6
View File
@@ -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)
}
})
}
+33
View File
@@ -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,
})
}
+5
View File
@@ -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 {
+21
View File
@@ -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})
+5
View File
@@ -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
+3
View File
@@ -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})
} }
+1 -1
View File
@@ -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
} }
} }
+6 -6
View File
@@ -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
} }
+1 -12
View File
@@ -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)
+2 -2
View File
@@ -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" .}}
+2 -2
View File
@@ -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" .}}
+2 -2
View File
@@ -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" .}}
+8 -6
View File
@@ -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" .}}
+10 -8
View File
@@ -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" .}}
+4 -4
View File
@@ -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}}
+2 -2
View File
@@ -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" .}}
+11 -9
View File
@@ -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