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
;DEFAULT_MERGE_MESSAGE_OFFICIAL_APPROVERS_ONLY = true
;;
;; In default squash-merge messages include the commit message of all commits comprising the pull request.
;POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES = false
;; Set default squash merge commit message body, valid options: PR_BODY (pull request description), COMMIT_MESSAGES (commit messages), BLANK (none)
;; 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_COMMITTER_TRAILERS = true
@@ -1433,11 +1434,14 @@ LEVEL = Info
;; The icon theme for folders (basic/material)
;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.
;; 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 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).
;REACTION_MAX_USER_NUM = 10
;;
+1 -1
View File
@@ -2,7 +2,7 @@ module gitea.dev
go 1.27
toolchain go1.27.2
toolchain go1.27.3
require (
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(361, "Drop legacy commit_id column from comment", v29.DropCommentCommitID),
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
}
+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 {
return nil
}
c.Reactions, _, err = FindReactions(ctx, FindReactionsOptions{
IssueID: c.IssueID,
CommentID: c.ID,
})
c.Reactions, _, err = FindReactions(ctx, FindReactionsOptions{CommentID: c.ID})
if err != nil {
return err
}
@@ -1210,7 +1207,7 @@ func DeleteComment(ctx context.Context, comment *Comment) error {
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
+3 -5
View File
@@ -225,13 +225,11 @@ func (issue *Issue) loadCommentsByType(ctx context.Context, tp CommentType) (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 {
return nil
}
reactions, _, err := FindReactions(ctx, FindReactionsOptions{
IssueID: issue.ID,
})
reactions, _, err := FindReactions(ctx, FindReactionsOptions{IssueIDWithComments: issue.ID})
if err != nil {
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
+84 -110
View File
@@ -58,17 +58,20 @@ func (err ErrReactionAlreadyExist) Unwrap() error {
return util.ErrAlreadyExist
}
// Reaction represents a reactions on issues and comments.
// Reaction represents a reaction on an issue, comment or release.
type Reaction struct {
ID int64 `xorm:"pk autoincr"`
Type string `xorm:"INDEX UNIQUE(s) NOT NULL"`
IssueID int64 `xorm:"INDEX UNIQUE(s) NOT NULL"`
CommentID int64 `xorm:"INDEX UNIQUE(s)"`
UserID int64 `xorm:"INDEX UNIQUE(s) NOT NULL"`
OriginalAuthorID int64 `xorm:"INDEX UNIQUE(s) NOT NULL DEFAULT(0)"`
OriginalAuthor string `xorm:"INDEX UNIQUE(s)"`
User *user_model.User `xorm:"-"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
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"`
User *user_model.User `xorm:"-"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
}
// LoadUser load user of reaction
@@ -108,53 +111,48 @@ func init() {
// FindReactionsOptions describes the conditions to Find reactions
type FindReactionsOptions struct {
db.ListOptions
IssueID int64
CommentID int64
UserID int64
Reaction string
IssueIDOnly int64
IssueIDWithComments int64
CommentID int64
ReleaseID int64
UserID int64
}
func (opts *FindReactionsOptions) toConds() builder.Cond {
// If Issue ID is set add to Query
cond := builder.NewCond()
if opts.IssueID > 0 {
cond = cond.And(builder.Eq{"reaction.issue_id": opts.IssueID})
if opts.IssueIDOnly > 0 {
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 {
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 {
cond = cond.And(builder.Eq{
"reaction.user_id": opts.UserID,
"reaction.original_author_id": 0,
})
cond = cond.And(builder.Eq{"reaction.user_id": opts.UserID, "reaction.original_author_id": 0})
}
if opts.Reaction != "" {
cond = cond.And(builder.Eq{"reaction.type": opts.Reaction})
if !cond.IsValid() {
setting.PanicInDevOrTesting("FindReactionsOptions.toConds: no conditions specified")
cond = cond.And(builder.Expr("1=0"))
}
return cond
}
// FindCommentReactions returns a ReactionList of all reactions from an comment
func FindCommentReactions(ctx context.Context, issueID, commentID int64) (ReactionList, int64, error) {
return FindReactions(ctx, FindReactionsOptions{
IssueID: issueID,
CommentID: commentID,
})
// FindCommentReactions returns a ReactionList of all reactions from a comment
func FindCommentReactions(ctx context.Context, commentID int64) (ReactionList, int64, error) {
return FindReactions(ctx, FindReactionsOptions{CommentID: commentID})
}
// FindIssueReactions returns a ReactionList of all reactions from an issue
func FindIssueReactions(ctx context.Context, issueID int64, listOptions db.ListOptions) (ReactionList, int64, error) {
return FindReactions(ctx, FindReactionsOptions{
ListOptions: listOptions,
IssueID: issueID,
CommentID: -1,
IssueIDOnly: issueID,
})
}
@@ -177,37 +175,14 @@ func FindReactions(ctx context.Context, opts FindReactionsOptions) (ReactionList
return reactions, count, err
}
func createReaction(ctx context.Context, opts *ReactionOptions) (*Reaction, error) {
reaction := &Reaction{
Type: opts.Type,
UserID: opts.DoerID,
IssueID: opts.IssueID,
CommentID: opts.CommentID,
}
findOpts := FindReactionsOptions{
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
func FindReleaseReactions(ctx context.Context, releaseIDs ...int64) (ReactionList, error) {
reactions := make(ReactionList, 0, 10)
err := db.GetEngine(ctx).
In("release_id", releaseIDs).
In("`type`", setting.UI.ReleaseReactions).
Asc("created_unix", "id").
Find(&reactions)
return reactions, err
}
// ReactionOptions defines options for creating or deleting reactions
@@ -216,56 +191,63 @@ type ReactionOptions struct {
DoerID int64
IssueID int64
CommentID int64
ReleaseID int64
}
// CreateReaction creates reaction for issue or comment.
func CreateReaction(ctx context.Context, opts *ReactionOptions) (*Reaction, error) {
if !setting.UI.ReactionsLookup.Contains(opts.Type) {
// CreateReaction creates reaction for issue, comment or release.
func CreateReaction(ctx context.Context, allowed container.Set[string], opts *ReactionOptions) (*Reaction, error) {
if !allowed.Contains(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{
Type: opts.Type,
UserID: opts.DoerID,
IssueID: opts.IssueID,
CommentID: opts.CommentID,
ReleaseID: opts.ReleaseID,
}
sess := db.GetEngine(ctx).Where("original_author_id = 0")
if opts.CommentID == -1 {
reaction.CommentID = 0
sess.MustCols("comment_id")
err := db.Insert(ctx, reaction)
if err != nil {
if has, _ := db.GetEngine(ctx).Get(reaction); has {
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
}
// DeleteIssueReaction deletes a reaction on issue.
func DeleteIssueReaction(ctx context.Context, doerID, issueID int64, content string) error {
return DeleteReaction(ctx, &ReactionOptions{
Type: content,
DoerID: doerID,
IssueID: issueID,
CommentID: -1,
})
func DeleteCommentReaction(ctx context.Context, doerID, issueID, commentID 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)
_, err := sess.Delete(&Reaction{})
return err
}
// DeleteCommentReaction deletes a reaction on comment.
func DeleteCommentReaction(ctx context.Context, doerID, issueID, commentID int64, content string) error {
return DeleteReaction(ctx, &ReactionOptions{
Type: content,
DoerID: doerID,
IssueID: issueID,
CommentID: commentID,
})
func DeleteCommentAllReactions(ctx context.Context, commentID int64) error {
sess := db.GetEngine(ctx).Where("comment_id = ?", commentID)
_, err := sess.Delete(&Reaction{})
return err
}
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
@@ -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
func newMigrationOriginalUser(name string) *user_model.User {
return &user_model.User{ID: 0, Name: name, LowerName: strings.ToLower(name)}
}
// 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 {
return nil, nil
return map[int64]*user_model.User{}, nil
}
userIDs := list.getUserIDs()
@@ -339,7 +313,7 @@ func (list ReactionList) LoadUsers(ctx context.Context, repo *repo_model.Reposit
reaction.User = user_model.NewGhostUser()
}
}
return valuesUser(userMaps), nil
return userMaps, nil
}
// GetFirstUsers returns first reacted user display names separated by comma
@@ -350,7 +324,7 @@ func (list ReactionList) GetFirstUsers() string {
if buffer.Len() > 0 {
buffer.WriteString(", ")
}
buffer.WriteString(reaction.User.Name)
buffer.WriteString(reaction.User.DisplayNameWithFullName())
if rem--; rem == 0 {
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
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{
Types: []ReviewType{ReviewTypePending},
IssueID: issue.ID,
@@ -442,9 +439,10 @@ func GetCurrentReview(ctx context.Context, reviewer *user_model.User, issue *Iss
if len(reviews) == 0 {
return nil, ErrReviewNotExist{}
}
reviews[0].Reviewer = reviewer
reviews[0].Issue = issue
return reviews[0], nil
pendingReview := reviews[0]
pendingReview.Reviewer = reviewer
pendingReview.Issue = issue
return pendingReview, nil
}
// 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"
)
// 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
func UpdateDefaultBranch(ctx context.Context, repo *Repository) error {
_, err := db.GetEngine(ctx).ID(repo.ID).Cols("default_branch").Update(repo)
+2
View File
@@ -131,6 +131,7 @@ type PullRequestsConfig struct {
DefaultUpdateStyle UpdateStyle
DefaultDeleteBranchAfterMerge bool
DefaultMergeStyle MergeStyle
DefaultSquashMergeMessage SquashMergeMessage
DefaultAllowMaintainerEdit bool
DefaultTargetBranch string
}
@@ -150,6 +151,7 @@ func DefaultPullRequestsConfig() *PullRequestsConfig {
cfg.DefaultDeleteBranchAfterMerge = setting.Repository.PullRequest.DefaultDeleteBranchAfterMerge
cfg.DefaultMergeStyle = MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle)
cfg.DefaultMergeStyle = util.IfZero(cfg.DefaultMergeStyle, MergeStyleMerge)
cfg.DefaultSquashMergeMessage = SquashMergeMessage(setting.Repository.PullRequest.DefaultSquashMergeMessage)
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) {
if watch.UserID <= 0 {
return nil
}
if watch.Mode == mode {
return nil
}
+10 -4
View File
@@ -34,7 +34,7 @@ func TestGetWatchers(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
watches, err := repo_model.GetWatchers(t.Context(), repo.ID)
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)
for _, watch := range watches {
assert.Equal(t, repo.ID, watch.RepoID)
@@ -90,24 +90,30 @@ func TestWatchIfAuto(t *testing.T) {
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))
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
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))
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
assert.NoError(t, err)
assert.Len(t, watchers, prevCount)
// Should add watch
// Should add watch (write operation)
assert.NoError(t, repo_model.WatchIfAuto(t.Context(), 12, 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 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
assert.NoError(t, repo_model.WatchRepoAuto(t.Context(), user12, repo, false))
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
// Search this TODO to see more details
// * user.Name / user.FullName: directly used in templates
// * 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
@@ -491,6 +492,14 @@ func (u *User) GetDisplayName() string {
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
func (u *User) ShortName(length int) string {
return util.EllipsisDisplayString(u.GetDisplayName(), length)
+6 -11
View File
@@ -295,18 +295,13 @@ func TestNewGitSig(t *testing.T) {
}
func TestDisplayName(t *testing.T) {
users := make([]*user_model.User, 0, 20)
err := db.GetEngine(t.Context()).Find(&users)
assert.NoError(t, err)
u := &user_model.User{Name: "name"}
assert.Equal(t, "name", u.DisplayName())
assert.Equal(t, "name", u.DisplayNameWithFullName())
for _, user := range users {
displayName := user.DisplayName()
assert.Equal(t, strings.TrimSpace(displayName), displayName)
if len(strings.TrimSpace(user.FullName)) == 0 {
assert.Equal(t, user.Name, displayName)
}
assert.NotEmpty(t, strings.TrimSpace(displayName))
}
u = &user_model.User{Name: "name", FullName: "Full Name"}
assert.Equal(t, "Full Name", u.DisplayName())
assert.Equal(t, "name (Full Name)", u.DisplayNameWithFullName())
}
func TestCreateUserEmailAlreadyUsed(t *testing.T) {
+11 -2
View File
@@ -7,19 +7,28 @@ import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"strings"
"gitea.dev/modules/git/gitcmd"
"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).
AddOptionFormat("-L %d,%d", line, line).
AddOptionValues("-p", revision).
AddDashesAndList(file).
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
+12
View File
@@ -11,6 +11,18 @@ import (
"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) {
setting.AppDataPath = t.TempDir()
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.
// 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) {
genCmd := func(reversions ...string) *gitcmd.Command {
cmd := gitcmd.NewCommand("rev-list", "--reverse").
AddArguments("-n").AddDynamicArguments(strconv.Itoa(limit)).
AddDynamicArguments(reversions...)
if notRef != "" { // --not should be kept as the last parameter of git command, otherwise the result will be wrong
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)
cmd := gitcmd.NewCommand("rev-list", "--reverse").
AddArguments("-n").AddDynamicArguments(strconv.Itoa(limit)).
AddDynamicArguments(startRef + ".." + endRef)
if notRef != "" { // --not should be kept as the last parameter of git command, otherwise the result will be wrong
cmd.AddOptionValues("--not", notRef)
}
stdout, _, err := cmd.WithRepo(repo).RunStdString(ctx)
if err != nil {
return nil, err
}
+13 -17
View File
@@ -5,6 +5,7 @@ package git
import (
"bufio"
"cmp"
"context"
"fmt"
"io"
@@ -200,22 +201,18 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
}
if old {
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
otherLine, _ = strconv.ParseInt(groups["beginNew"], 10, 64)
} else {
begin, _ = strconv.ParseInt(groups["beginNew"], 10, 64)
if groups["endNew"] != "" {
end, _ = strconv.ParseInt(groups["endNew"], 10, 64)
} else {
end = 0
}
end, _ = strconv.ParseInt(cmp.Or(groups["endNew"], "1"), 10, 64)
// init otherLine with begin of opposite side
otherLine, _ = strconv.ParseInt(groups["beginOld"], 10, 64)
}
end += begin // end is for real only the number of lines in hunk
// lof is between begin and end
if begin <= line && end >= line {
if begin <= line && line < end {
hunk = append(hunk, lof)
currentLine = begin
continue
@@ -265,27 +262,26 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
oldBegin = otherLine
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
for i := len(hunk) - 1; i >= len(hunk)-numbersOfLine; i-- {
switch hunk[i][0] {
start := len(hunk)
for count := 0; count < numbersOfLine && start > headerLines+1; {
start--
switch hunk[start][0] {
case '+':
newBegin--
newNumOfLines++
case '-':
oldBegin--
oldNumOfLines++
case '\\':
continue
default:
oldBegin--
newBegin--
newNumOfLines++
oldNumOfLines++
}
count++
}
// "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)
// 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)
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
+9 -1
View File
@@ -120,10 +120,18 @@ func TestCutDiffAroundLine(t *testing.T) {
assert.Empty(t, emptyResult)
// 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.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
minusDiff, err := CutDiffAroundLine(strings.NewReader(breakingDiff), 2, false, 4)
assert.NoError(t, err)
+7 -3
View File
@@ -68,12 +68,12 @@ func DefaultFeatures() *Features {
func loadGitVersionFeatures() (*Features, error) {
stdout, _, runErr := gitcmd.NewCommand("version").RunStdString(context.Background())
if runErr != nil {
return nil, runErr
return nil, fmt.Errorf(`failed to run command "git version": %w`, runErr)
}
ver, err := parseGitVersionLine(strings.TrimSpace(stdout))
if err != nil {
return nil, err
return nil, fmt.Errorf(`failed to parse output of command "git version": %w`, err)
}
features := &Features{gitVersion: ver}
@@ -104,7 +104,11 @@ func parseGitVersionLine(s string) (*version.Version, error) {
if len(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 {
+2 -5
View File
@@ -93,15 +93,12 @@ const (
StderrPathSpec StderrPrefix = "fatal: pathspec"
StderrBadRevision StderrPrefix = "fatal: bad revision"
StderrNoSuchPath StderrPrefix = "fatal: no such path"
StderrNoSuchRemote StderrPrefix = "error: no such remote"
StderrBadObject StderrPrefix = "fatal: bad object "
StderrAuthenticationFailed StderrPrefix = "fatal: Authentication failed for"
StderrCouldNotReadUsername StderrPrefix = "fatal: could not read Username"
StderrUnknownRevisionOrPath StderrRegexp = "^fatal: .*: unknown revision or path not in the working tree"
StderrNoMergeBase StderrRegexp = "^fatal: .*: no merge base"
StderrFileNoEnoughLines StderrRegexp = `^fatal: file .* has only \d+ lines?`
StderrFileNoEnoughLines StderrRegexp = `^fatal: file .* has only \d+ lines?`
)
func matchStderrCheck(stderr string, checkIntf StderrCheck) (match bool) {
-2
View File
@@ -14,8 +14,6 @@ func TestIsStderr(t *testing.T) {
check StderrCheck
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"},
{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
}
// 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 {
return gitcmd.IsStderr(err, gitcmd.StderrNoSuchRemote)
return gitcmd.IsErrorExitCode(err, 2)
}
// 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.
// 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) {
gitCmd := func() *gitcmd.Command {
cmd := gitcmd.NewCommand("rev-list").WithRepo(repo)
if limit >= 0 {
cmd.AddOptionValues("--max-count", strconv.Itoa(limit))
}
if len(optSkip) > 0 {
cmd.AddOptionValues("--skip", strconv.Itoa(optSkip[0]))
}
return cmd
cmd := gitcmd.NewCommand("rev-list").WithRepo(repo)
if limit >= 0 {
cmd.AddOptionValues("--max-count", strconv.Itoa(limit))
}
if len(optSkip) > 0 {
cmd.AddOptionValues("--skip", strconv.Itoa(optSkip[0]))
}
var stdout []byte
var err error
if beforeRef == "" {
stdout, _, err = gitCmd().AddDynamicArguments(afterRef.String()).RunStdBytes(ctx)
cmd.AddDynamicArguments(afterRef.String())
} else {
stdout, _, err = gitCmd().AddDynamicArguments(beforeRef.String() + ".." + afterRef.String()).RunStdBytes(ctx)
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)
}
cmd.AddDynamicArguments(beforeRef.String() + ".." + afterRef.String())
}
stdout, _, err := cmd.RunStdBytes(ctx)
if err != nil {
return nil, err
}
-5
View File
@@ -46,11 +46,6 @@ func (repo *Repository) GetDiffNumChangedFiles(ctx context.Context, base, head s
WithRepo(repo).
WithStdoutCopy(w).
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 w.numLines, nil
+10 -24
View File
@@ -83,7 +83,7 @@ type Tree struct {
}
// 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 {
return t.entries, nil
}
@@ -109,36 +109,22 @@ func (t *Tree) ListEntries(ctx context.Context, gitRepo *Repository) (Entries, e
return nil, err
}
}
if info.Type == "tree" {
t.entries, err = catBatchParseTreeEntries(t.ID.Type(), t, rd, info.Size)
if info.Type != "tree" { // resolve annotated tags to their tree, a blob yields ErrNotExist
if err := DiscardFull(rd, info.Size+1); err != nil {
return nil, err
}
info, rd, err = batch.QueryContent(t.ID.String() + "^{tree}")
if err != nil {
return nil, err
}
t.entriesParsed = true
return t.entries, nil
}
// Not a tree just use ls-tree instead
if err := DiscardFull(rd, info.Size+1); err != nil {
t.entries, err = catBatchParseTreeEntries(t.ID.Type(), t, rd, info.Size)
if err != nil {
return nil, err
}
stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-l").AddDynamicArguments(t.ID.String()).WithRepo(gitRepo).RunStdBytes(ctx)
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
t.entriesParsed = true
return t.entries, nil
}
// listEntriesRecursive returns all entries of current tree recursively including all subtrees
+19
View File
@@ -8,6 +8,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
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) {
repo, err := OpenRepositoryLocal(t.Context(), filepath.Join(testReposDir, "repo6_blame"))
assert.NoError(t, err)
+17
View File
@@ -8,6 +8,8 @@ import (
"compress/gzip"
"io"
"regexp"
"slices"
"strings"
"sync"
"gitea.dev/modules/packages"
@@ -24,6 +26,8 @@ var (
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
// ErrInvalidVersion indicates an invalid version in the metadata.gz file
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 {
@@ -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{
Name: spec.Name,
Version: spec.Version.Version,
@@ -43,6 +43,19 @@ version:
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) {
+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)
}
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) {
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("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() {
GitConfig.SetOption(key.Name(), key.String())
}
+1
View File
@@ -56,6 +56,7 @@ ENABLED = false
EXPIRATION = 123
`)
assert.NoError(t, err)
applyDeprecatedKeys(cfg)
loadGitFrom(cfg)
assert.Equal(t, "false", GitConfig.GetOption("core.logAllRefUpdates"))
-12
View File
@@ -40,18 +40,6 @@ func loadLFSFrom(rootCfg ConfigProvider) error {
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
LFS.Storage, err = getStorage(rootCfg, "lfs", "", lfsSec)
if err != nil {
+2
View File
@@ -29,6 +29,7 @@ PATH = path_used
`
cfg, err = NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
applyDeprecatedKeys(cfg)
assert.NoError(t, loadLFSFrom(cfg))
assert.EqualValues(t, "local", LFS.Storage.Type)
@@ -40,6 +41,7 @@ LFS_CONTENT_PATH = deprecatedpath
`
cfg, err = NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
applyDeprecatedKeys(cfg)
assert.NoError(t, loadLFSFrom(cfg))
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
}
deprecatedSetting(rootCfg, "log", "ACCESS", "log", "logger.access.MODE", "1.21")
deprecatedSetting(rootCfg, "log", "ENABLE_ACCESS_LOG", "log", "logger.access.MODE", "1.21")
deprecatedSetting(rootCfg, "log", "ACCESS", "log", "logger.access.MODE")
deprecatedSetting(rootCfg, "log", "ENABLE_ACCESS_LOG", "log", "logger.access.MODE")
if val := sec.Key("ACCESS").String(); val != "" {
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("")
}
deprecatedSetting(rootCfg, "log", "ROUTER", "log", "logger.router.MODE", "1.21")
deprecatedSetting(rootCfg, "log", "DISABLE_ROUTER_LOG", "log", "logger.router.MODE", "1.21")
deprecatedSetting(rootCfg, "log", "ROUTER", "log", "logger.router.MODE")
deprecatedSetting(rootCfg, "log", "DISABLE_ROUTER_LOG", "log", "logger.router.MODE")
if val := sec.Key("ROUTER").String(); val != "" {
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("")
}
deprecatedSetting(rootCfg, "log", "XORM", "log", "logger.xorm.MODE", "1.21")
deprecatedSetting(rootCfg, "log", "ENABLE_XORM_LOG", "log", "logger.xorm.MODE", "1.21")
deprecatedSetting(rootCfg, "log", "XORM", "log", "logger.xorm.MODE")
deprecatedSetting(rootCfg, "log", "ENABLE_XORM_LOG", "log", "logger.xorm.MODE")
if val := sec.Key("XORM").String(); val != "" {
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
// 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, "mailer", "MAILER_TYPE", "mailer", "PROTOCOL", "v1.19.0")
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")
deprecatedSetting(rootCfg, "mailer", "HOST", "mailer", "SMTP_ADDR")
if sec.HasKey("HOST") && !sec.HasKey("SMTP_ADDR") {
givenHost := sec.Key("HOST").String()
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
}
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" {
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")
}
+3 -3
View File
@@ -27,9 +27,9 @@ func loadMigrationsFrom(rootCfg ConfigProvider) {
egressModeSet := sec.HasKey("EGRESS_MODE")
Migrations.EgressMode = normalizePolicyMode(sec.Key("EGRESS_MODE").String())
deprecatedSetting(rootCfg, "migrations", "ALLOWED_DOMAINS", "migrations", "ALLOWED_HOST_LIST", "v28.0.0")
deprecatedSetting(rootCfg, "migrations", "BLOCKED_DOMAINS", "migrations", "BLOCKED_HOST_LIST", "v28.0.0")
deprecatedSetting(rootCfg, "migrations", "ALLOW_LOCALNETWORKS", "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")
deprecatedSetting(rootCfg, "migrations", "ALLOW_LOCALNETWORKS", "migrations", "ALLOWED_HOST_LIST")
Migrations.AllowedHostList = ConfigSectionKeyString(sec, "ALLOWED_HOST_LIST")
if Migrations.AllowedHostList == "" {
var hosts []string
-9
View File
@@ -25,15 +25,6 @@ var Mirror = struct {
}
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 {
log.Fatal("Failed to map Mirror settings: %v", err)
}
-6
View File
@@ -122,12 +122,6 @@ func loadOAuth2From(rootCfg ConfigProvider) {
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) {
OAuth2.JWTSigningPrivateKeyFile = filepath.Join(AppDataPath, OAuth2.JWTSigningPrivateKeyFile)
}
+8 -3
View File
@@ -6,6 +6,7 @@ package setting
import (
"os/exec"
"path/filepath"
"slices"
"strings"
"gitea.dev/modules/log"
@@ -94,7 +95,7 @@ var (
DefaultMergeMessageAllAuthors bool
DefaultMergeMessageMaxApprovers int
DefaultMergeMessageOfficialApproversOnly bool
PopulateSquashCommentWithCommitMessages bool
DefaultSquashMergeMessage string
AddCoCommitterTrailers bool
RetargetChildrenOnMerge bool
DelayCheckForInactiveDays int
@@ -188,7 +189,7 @@ var (
DefaultMergeMessageAllAuthors bool
DefaultMergeMessageMaxApprovers int
DefaultMergeMessageOfficialApproversOnly bool
PopulateSquashCommentWithCommitMessages bool
DefaultSquashMergeMessage string
AddCoCommitterTrailers bool
RetargetChildrenOnMerge bool
DelayCheckForInactiveDays int
@@ -206,7 +207,7 @@ var (
DefaultMergeMessageAllAuthors: false,
DefaultMergeMessageMaxApprovers: 10,
DefaultMergeMessageOfficialApproversOnly: true,
PopulateSquashCommentWithCommitMessages: false,
DefaultSquashMergeMessage: "PR_BODY",
AddCoCommitterTrailers: true,
RetargetChildrenOnMerge: true,
DelayCheckForInactiveDays: 7,
@@ -339,6 +340,10 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
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) {
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)
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 {
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")
HTTPPort = sec.Key("HTTP_PORT").MustString("3000")
// 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 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)
}
EnableAcme = sec.Key("ENABLE_ACME").MustBool(false)
protocolCfg := sec.Key("PROTOCOL").String()
if protocolCfg != "https" && EnableAcme {
@@ -175,29 +168,12 @@ func loadServerFrom(rootCfg ConfigProvider) {
AcmeProfile = sec.Key("ACME_PROFILE").MustString("")
AcmeCARoot = sec.Key("ACME_CA_ROOT").MustString("")
if sec.HasKey("ACME_ACCEPTTOS") {
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)
}
AcmeTOS = sec.Key("ACME_ACCEPTTOS").MustBool(false)
if !AcmeTOS {
log.Fatal("ACME TOS is not accepted (ACME_ACCEPTTOS).")
}
if sec.HasKey("ACME_DIRECTORY") {
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("")
}
AcmeLiveDirectory = sec.Key("ACME_DIRECTORY").MustString("https")
AcmeEmail = sec.Key("ACME_EMAIL").MustString("")
} else {
CertFile = sec.Key("CERT_FILE").String()
KeyFile = sec.Key("KEY_FILE").String()
+1 -1
View File
@@ -158,7 +158,7 @@ func loadServiceFrom(rootCfg ConfigProvider) {
Service.RegisterManualConfirm = false
}
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.EmailDomainBlockList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_BLOCKLIST")
+1
View File
@@ -117,6 +117,7 @@ func LoadCommonSettings() {
func loadCommonSettingsFrom(cfg ConfigProvider) error {
// a lot of logic depends on InstallLock value, so it must be loaded before any other settings
InstallLock = HasInstallLock(cfg)
applyDeprecatedKeys(cfg)
// WARNING: don't change the sequence except you know what you are doing.
loadRunModeFrom(cfg)
+3 -3
View File
@@ -10,9 +10,9 @@ func loadTaskFrom(rootCfg ConfigProvider) {
taskSec := rootCfg.Section("task")
queueTaskSec := rootCfg.Section("queue.task")
deprecatedSetting(rootCfg, "task", "QUEUE_TYPE", "queue.task", "TYPE", "v1.19.0")
deprecatedSetting(rootCfg, "task", "QUEUE_CONN_STR", "queue.task", "CONN_STR", "v1.19.0")
deprecatedSetting(rootCfg, "task", "QUEUE_LENGTH", "queue.task", "LENGTH", "v1.19.0")
deprecatedSetting(rootCfg, "task", "QUEUE_TYPE", "queue.task", "TYPE")
deprecatedSetting(rootCfg, "task", "QUEUE_CONN_STR", "queue.task", "CONN_STR")
deprecatedSetting(rootCfg, "task", "QUEUE_LENGTH", "queue.task", "LENGTH")
switch taskSec.Key("QUEUE_TYPE").MustString("channel") {
case "channel":
+6 -4
View File
@@ -30,7 +30,9 @@ var UI = struct {
FileIconTheme string
FolderIconTheme string
Reactions []string
ReleaseReactions []string
ReactionsLookup container.Set[string] `ini:"-"`
ReleaseReactionsLookup container.Set[string] `ini:"-"`
CustomEmojis []string
CustomEmojisMap map[string]string `ini:"-"`
EnabledEmojis []string
@@ -98,6 +100,7 @@ var UI = struct {
FileIconTheme: `material`,
FolderIconTheme: `basic`,
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`},
CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"},
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.
UI.OnlyShowRelevantRepos = sec.Key("ONLY_SHOW_RELEVANT_REPOS").MustBool(false)
UI.ReactionsLookup = make(container.Set[string])
for _, reaction := range UI.Reactions {
UI.ReactionsLookup.Add(reaction)
}
UI.ReactionsLookup = container.SetOf(UI.Reactions...)
UI.ReleaseReactionsLookup = container.SetOf(UI.ReleaseReactions...)
UI.CustomEmojisMap = make(map[string]string)
for _, emoji := range UI.CustomEmojis {
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.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)
if Webhook.AllowedHostList != Security.AllowedHostList {
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"`
DefaultMergeStyle string `json:"default_merge_style"`
DefaultUpdateStyle string `json:"default_update_style"`
SquashMergeCommitMessage string `json:"squash_merge_commit_message"`
DefaultAllowMaintainerEdit bool `json:"default_allow_maintainer_edit"`
AvatarURL string `json:"avatar_url"`
Internal bool `json:"internal"`
@@ -236,6 +237,8 @@ type EditRepoOption struct {
DefaultMergeStyle *string `json:"default_merge_style,omitempty"`
// set to an update style to be used by this repository: "merge" or "rebase".
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
DefaultAllowMaintainerEdit *bool `json:"default_allow_maintainer_edit,omitempty"`
// set to `true` to archive this repository.
-3
View File
@@ -93,9 +93,6 @@ func newFuncMapWebPage() template.FuncMap {
"ShowFooterPoweredBy": func() bool {
return setting.Other.ShowFooterPoweredBy
},
"AllowedReactions": func() []string {
return setting.UI.Reactions
},
"MetaAuthor": func() string {
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_rebase_update": "Enable updating pull request branch by rebase",
"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": "Default branch (%s)",
"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.show_more": "Voir plus",
"user.starred": "Dépôts favoris",
"user.watched": "Dépôts surveillés",
"user.watched": "Dépôts observés",
"user.code": "Code",
"user.projects": "Projets",
"user.overview": "Vue d'ensemble",
@@ -929,6 +929,7 @@
"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.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.submit": "Définir les préférences de courriel",
"settings.email_notifications.andyourown": "Inclure vos propres notifications",
@@ -1141,10 +1142,10 @@
"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_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.watch": "Suivre",
"repo.watching": "Suivi",
"repo.watch": "Observer",
"repo.watching": "Observé",
"repo.ignoring": "Ignoré",
"repo.watch.options.issues": "Tickets",
"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.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_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_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.",
@@ -3548,7 +3549,7 @@
"notification.mark_all_as_read": "Tout marquer comme lu",
"notification.mark_page_as_read": "Marquer cette page comme lue",
"notification.subscriptions": "Abonnements",
"notification.watching": "Suivi",
"notification.watching": "Observé",
"notification.no_subscriptions": "Pas d'abonnements",
"gpg.default_key": "Signé avec la clé par défaut",
"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_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.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.submit": "Socraigh rogha ríomhphoist",
"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.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.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_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",
+11
View File
@@ -87,6 +87,7 @@
"add_all": "すべて追加",
"dismiss": "閉じる",
"remove": "除去",
"rename": "名前を変更",
"remove_all": "すべて除去",
"remove_label_str": "アイテム「%s」を削除",
"edit": "編集",
@@ -906,6 +907,7 @@
"settings.webauthn_desc": "セキュリティキーは暗号化キーを内蔵するハードウェア ・ デバイスです。 2要素認証に使用できます。 セキュリティキーは<a rel=\"noreferrer\" target=\"_blank\" href=\"%s\">WebAuthn Authenticator</a>規格をサポートしている必要があります。",
"settings.webauthn_register_key": "セキュリティキーを追加",
"settings.webauthn_nickname": "ニックネーム",
"settings.webauthn_nickname_been_used": "同じニックネームのシークレットキーが既に存在しています。",
"settings.webauthn_delete_key": "セキュリティキーの登録解除",
"settings.webauthn_delete_key_desc": "セキュリティキーの登録を解除すると、今後そのセキュリティキーでサインインすることはできなくなります。 続行しますか?",
"settings.webauthn_key_loss_warning": "セキュリティキーを紛失すると、アカウントへのアクセスを失います。",
@@ -2296,6 +2298,7 @@
"repo.settings.event_release_desc": "リポジトリでリリースが作成・更新・削除されたとき。",
"repo.settings.event_push": "プッシュ",
"repo.settings.event_force_push": "強制プッシュ",
"repo.settings.event_branch_delete": "ブランチの抹消",
"repo.settings.event_push_desc": "Gitがリポジトリにプッシュを行ったとき。",
"repo.settings.event_repository": "リポジトリ",
"repo.settings.event_repository_desc": "リポジトリが作成・削除・名前変更されたとき。",
@@ -2407,6 +2410,12 @@
"repo.settings.protect_enable_force_push_all_desc": "プッシュ権限を持つ人は誰でも、このブランチに強制プッシュを行うことができます。",
"repo.settings.protect_enable_force_push_allowlist": "許可リストで強制プッシュを制限",
"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_desc": "書き込み権限を持つ人は誰でも、このブランチにプルリクエストをマージすることができます。",
"repo.settings.protect_whitelist_committers": "許可リストでプッシュを制限",
@@ -2417,6 +2426,8 @@
"repo.settings.protect_force_push_allowlist_users": "強制プッシュ許可リストに含むユーザー:",
"repo.settings.protect_force_push_allowlist_teams": "強制プッシュ許可リストに含むチーム:",
"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_desc": "許可リストに登録したユーザーまたはチームにのみ、このブランチへのプルリクエストのマージを許可します。",
"repo.settings.protect_merge_whitelist_users": "マージ許可リストに含むユーザー:",
+3
View File
@@ -929,6 +929,7 @@
"settings.delete_account_title": "Eliminar 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.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.submit": "Definir preferência do email",
"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.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.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_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",
+12
View File
@@ -929,6 +929,7 @@
"settings.delete_account_title": "删除当前帐户",
"settings.delete_account_desc": "确定要永久删除此用户帐户吗?",
"settings.email_notifications.enable": "启用邮件通知",
"settings.email_notifications.onmention_and_custom": "仅在被提及或发生自定义关注事件时发送邮件",
"settings.email_notifications.disable": "停用邮件通知",
"settings.email_notifications.submit": "设置邮件通知",
"settings.email_notifications.andyourown": "同时接收由自己操作触发的通知",
@@ -2298,6 +2299,7 @@
"repo.settings.event_release_desc": "发布、更新或删除版本时。",
"repo.settings.event_push": "推送",
"repo.settings.event_force_push": "强制推送",
"repo.settings.event_branch_delete": "分支删除",
"repo.settings.event_push_desc": "Git 仓库推送",
"repo.settings.event_repository": "仓库",
"repo.settings.event_repository_desc": "仓库被创建、删除或重命名。",
@@ -2409,6 +2411,12 @@
"repo.settings.protect_enable_force_push_all_desc": "任何拥有推送权限的人都可以对这个分支进行强制推送。",
"repo.settings.protect_enable_force_push_allowlist": "强制推送白名单",
"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_desc": "任何具有写入权限的人都可以将合并请求合并到此分支中。",
"repo.settings.protect_whitelist_committers": "受白名单限制的推送",
@@ -2419,6 +2427,8 @@
"repo.settings.protect_force_push_allowlist_users": "允许强制推送的白名单用户:",
"repo.settings.protect_force_push_allowlist_teams": "允许强制推送的白名单团队:",
"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_desc": "仅允许白名单用户或团队合并合并请求到此分支。",
"repo.settings.protect_merge_whitelist_users": "合并白名单用户:",
@@ -3991,6 +4001,8 @@
"actions.general.token_permissions.maximum.description": "工作流任务的实际权限将受到最大权限的限制。",
"actions.general.token_permissions.fork_pr_note": "如果任务是由来自派生仓库的合并请求触发的,那么它的实际权限不会超过只读权限。",
"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_desc": "允许此所有者下的所有仓库在运行工作流任务时,通过 GITEA_TOKEN 以只读方式访问所选仓库。",
"actions.general.cross_repo_selected": "选择的仓库",
+2 -2
View File
@@ -335,8 +335,8 @@ func ListIssues(ctx *context.APIContext) {
keyword := ctx.FormTrim("q")
var labelIDs []int64
if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, splitted)
if labelsParam := ctx.FormTrim("labels"); labelsParam != "" {
labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, strings.Split(labelsParam, ","))
if err != nil {
ctx.APIErrorInternal(err)
return
+1 -1
View File
@@ -73,7 +73,7 @@ func GetIssueCommentReactions(ctx *context.APIContext) {
return
}
reactions, _, err := issues_model.FindCommentReactions(ctx, comment.IssueID, comment.ID)
reactions, _, err := issues_model.FindCommentReactions(ctx, comment.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
+12 -3
View File
@@ -981,14 +981,23 @@ func MergePullRequest(ctx *context.APIContext) {
}
message := strings.TrimSpace(form.MergeTitleField)
if len(message) == 0 {
message, _, err = pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pr, repo_model.MergeStyle(form.Do))
useDefaultSquashBody := !form.HasMergeMessageField && !ctx.Req.Form.Has("merge_message_field") && repo_model.MergeStyle(form.Do) == repo_model.MergeStyleSquash
if len(message) == 0 || useDefaultSquashBody {
defaultTitle, defaultBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pr, repo_model.MergeStyle(form.Do))
if err != nil {
ctx.APIErrorInternal(err)
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)
if len(form.MergeMessageField) > 0 {
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.AssignPtrString(changed, &config.DefaultMergeStyle, opts.DefaultMergeStyle)
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,
// so unrelated PATCH calls don't reject historical configs.
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["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment")
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/markup/markdown"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/modules/templates"
"gitea.dev/modules/util"
@@ -45,7 +46,7 @@ const (
tplIssueView templates.TplName = "repo/issue/view"
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"
issueTemplateTitleKey = "IssueTemplateTitle"
@@ -536,25 +537,20 @@ func ChangeIssueReaction(ctx *context.Context) {
return
}
if len(issue.Reactions) == 0 {
ctx.JSON(http.StatusOK, map[string]any{
"empty": true,
"html": "",
})
return
}
renderReactions(ctx, fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index), issue.Reactions, setting.UI.Reactions)
}
func renderReactions(ctx *context.Context, actionURL string, reactions issues_model.ReactionList, allowedReactions []string) {
html, err := ctx.RenderToHTML(tplReactions, map[string]any{
"ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index),
"Reactions": issue.Reactions.GroupByType(),
"ActionURL": actionURL,
"Reactions": reactions.GroupByType(),
"AllowedReactions": allowedReactions,
})
if err != nil {
ctx.ServerError("ChangeIssueReaction.HTMLString", err)
ctx.ServerError("RenderToHTML", err)
return
}
ctx.JSON(http.StatusOK, map[string]any{
"html": html,
})
ctx.JSON(http.StatusOK, map[string]any{"html": html})
}
// GetIssueAttachments returns attachments for the issue
+1 -19
View File
@@ -300,25 +300,7 @@ func ChangeCommentReaction(ctx *context.Context) {
return
}
if len(comment.Reactions) == 0 {
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,
})
renderReactions(ctx, fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID), comment.Reactions, setting.UI.Reactions)
}
// 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)
ctx.Data["IsProjectsEnabled"] = isProjectsEnabled
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment")
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["IsAttachmentEnabled"] = setting.Attachment.Enabled
ctx.Data["AllowedIssueReactions"] = setting.UI.Reactions
upload.AddUploadContext(ctx, "comment")
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
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 {
isKnownErrorForBroken := errors.Is(err, util.ErrNotExist) || gitcmd.IsStderr(err, gitcmd.StderrNotValidObjectName) || gitcmd.IsStderr(err, gitcmd.StderrUnknownRevisionOrPath)
if !isKnownErrorForBroken {
if !errors.Is(err, util.ErrNotExist) && !gitcmd.IsStderr(err, gitcmd.StderrNotValidObjectName) {
log.Error("GetCompareInfo: %v", err)
}
prInfo.IsPullRequestBroken = true
@@ -885,35 +884,39 @@ func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
}
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
currentReview, err := issues_model.GetCurrentReview(ctx, ctx.Doer, issue)
if err != nil && !issues_model.IsErrReviewNotExist(err) {
ctx.ServerError("GetCurrentReview", err)
return
}
numPendingCodeComments := int64(0)
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)
var numPendingCodeComments int64
var currentPendingReview *issues_model.Review
if ctx.Doer != nil {
currentPendingReview, err = issues_model.GetCurrentReview(ctx, ctx.Doer, issue)
if err != nil && !issues_model.IsErrReviewNotExist(err) {
ctx.ServerError("GetCurrentReview", err)
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["IsIssuePoster"] = ctx.Doer != nil && issue.IsPoster(ctx.Doer.ID)
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
// For files changed page
PrepareBranchList(ctx)
if ctx.Written() {
return
}
ctx.Data["AllowedIssueReactions"] = setting.UI.Reactions
upload.AddUploadContext(ctx, "comment")
ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
+1 -2
View File
@@ -55,7 +55,6 @@ func RenderNewCodeCommentForm(ctx *context.Context) {
return
}
ctx.Data["AfterCommitID"] = pullHeadCommitID
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment")
ctx.HTML(http.StatusOK, tplNewComment)
}
@@ -186,7 +185,7 @@ func renderConversation(ctx *context.Context, comment *issues_model.Comment, ori
return
}
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
ctx.Data["AllowedIssueReactions"] = setting.UI.Reactions
upload.AddUploadContext(ctx, "comment")
ctx.Data["comments"] = comments
+85 -13
View File
@@ -14,6 +14,7 @@ import (
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/renderhelper"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unit"
@@ -73,6 +74,7 @@ type ReleaseInfo struct {
Release *repo_model.Release
CommitStatus *git_model.CommitStatus
CommitStatuses []*git_model.CommitStatus
Reactions issues_model.ReactionList
}
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
}
releaseIDs := make([]int64, 0, len(releases))
for _, release := range releases {
release.Repo = ctx.Repo.Repository
releaseIDs = append(releaseIDs, release.ID)
}
if err = repo_model.GetReleaseAttachments(ctx, releases...); err != nil {
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.
countCache := make(map[string]int64)
cacheUsers := make(map[int64]*user_model.User)
if ctx.Doer != nil {
cacheUsers[ctx.Doer.ID] = ctx.Doer
}
@@ -138,7 +154,8 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
}
info := &ReleaseInfo{
Release: r,
Release: r,
Reactions: reactionsByRelease[r.ID],
}
if canReadCode {
@@ -154,10 +171,22 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions)
return releaseInfos, nil
}
// Releases render releases list page
func Releases(ctx *context.Context) {
func prepareReleasePageCommon(ctx *context.Context) (ret struct {
writeAccess bool
},
) {
ctx.Data["PageIsReleaseList"] = true
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{
Page: ctx.FormInt("page"),
@@ -170,13 +199,10 @@ func Releases(ctx *context.Context) {
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{
ListOptions: listOptions,
// 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,
})
if err != nil {
@@ -275,17 +301,14 @@ func releasesOrTagsFeed(ctx *context.Context, isReleasesOnly bool, formatType st
// SingleRelease renders a single release's page
func SingleRelease(ctx *context.Context) {
ctx.Data["PageIsReleaseList"] = true
writeAccess := ctx.Repo.Permission.CanWrite(unit.TypeReleases)
ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived
preparedData := prepareReleasePageCommon(ctx)
releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{
ListOptions: db.ListOptions{Page: 1, PageSize: 1},
RepoID: ctx.Repo.Repository.ID,
TagNames: []string{ctx.PathParam("*")},
// only show draft releases for users who can write, read-only users shouldn't see draft releases.
IncludeDrafts: writeAccess,
IncludeDrafts: preparedData.writeAccess,
IncludeTags: true,
})
if err != nil {
@@ -681,3 +704,52 @@ func deleteReleaseOrTag(ctx *context.Context, isDelTag bool) {
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,
DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge,
DefaultMergeStyle: repo_model.MergeStyle(form.PullsDefaultMergeStyle),
DefaultSquashMergeMessage: util.IfZero(repo_model.SquashMergeMessage(form.PullsDefaultSquashMergeMessage), repo_model.DefaultPullRequestsConfig().DefaultSquashMergeMessage),
DefaultAllowMaintainerEdit: form.DefaultAllowMaintainerEdit,
DefaultTargetBranch: strings.TrimSpace(form.DefaultTargetBranch),
}
@@ -687,6 +688,10 @@ func handleSettingsPostAdvanced(ctx *context.Context) {
ctx.JSONErrorAuto(err)
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))
} else if !unit_model.TypePullRequests.UnitGlobalDisabled() {
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 {
ctx.Redirect(middleware.RedirectLinkUserLogin(ctx.Req))
ctx.RedirectForLogin(middleware.RedirectLinkUserLogin(ctx.Req))
return
}
// 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) != "" {
ctx.Redirect(middleware.RedirectLinkUserLogin(ctx.Req))
ctx.RedirectForLogin(middleware.RedirectLinkUserLogin(ctx.Req))
return
}
@@ -1503,6 +1503,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
}, ctxDataSet(reqctx.ContextData{"EnableFeed": setting.Other.EnableFeed}))
m.Get("/releases/attachments/{uuid}", webAuth.AllowBasic, webAuth.AllowOAuth2, repo.GetAttachment)
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.Get("/new", repo.NewRelease)
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.Post("/comments", web.Bind[*forms.CodeCommentForm](), repo.SetShowOutdatedComments, repo.CreateCodeComment)
m.Post("/submit", web.Bind[*forms.SubmitReviewForm](), repo.SubmitReview)
}, context.RepoMustNotBeArchived())
}, reqSignIn, context.RepoMustNotBeArchived())
})
})
}, 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
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
// 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) {
b.JSON(http.StatusOK, map[string]any{"redirect": location})
return
}
code := util.OptionalArg(status, http.StatusSeeOther)
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
func (b *Base) SetServeHeaders(opts ServeHeaderOptions) {
+1
View File
@@ -104,6 +104,7 @@ type uploadOptions struct {
// AddUploadContext renders template values for dropzone
func AddUploadContext(ctx *context.Context, uploadType string) {
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
switch uploadType {
case "release":
ctx.Data["UploadOptions"] = uploadOptions{
+3
View File
@@ -105,6 +105,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
defaultDeleteBranchAfterMerge := false
defaultMergeStyle := repo_model.MergeStyleMerge
defaultUpdateStyle := repo_model.UpdateStyleMerge
defaultSquashMergeMessage := repo_model.DefaultPullRequestsConfig().DefaultSquashMergeMessage
defaultAllowMaintainerEdit := false
defaultTargetBranch := ""
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
defaultMergeStyle = config.DefaultMergeStyle
defaultUpdateStyle = config.DefaultUpdateStyle
defaultSquashMergeMessage = config.DefaultSquashMergeMessage
defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit
defaultTargetBranch = config.DefaultTargetBranch
}
@@ -251,6 +253,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge,
DefaultMergeStyle: string(defaultMergeStyle),
DefaultUpdateStyle: string(defaultUpdateStyle),
SquashMergeCommitMessage: string(defaultSquashMergeMessage),
DefaultAllowMaintainerEdit: defaultAllowMaintainerEdit,
DefaultTargetBranch: defaultTargetBranch,
AvatarURL: repo.AvatarLink(ctx),
+14 -9
View File
@@ -133,6 +133,7 @@ type RepoSettingForm struct {
PullsAllowMergeUpdate bool
PullsAllowRebaseUpdate bool
PullsDefaultUpdateStyle string
PullsDefaultSquashMergeMessage string
DefaultDeleteBranchAfterMerge bool
DefaultAllowMaintainerEdit bool
DefaultTargetBranch string
@@ -424,20 +425,21 @@ type MergePullRequestForm struct {
ForceMerge bool `json:"force_merge,omitempty"`
MergeWhenChecksSucceed bool `json:"merge_when_checks_succeed,omitempty"`
DeleteBranchAfterMerge *bool `json:"delete_branch_after_merge,omitempty"`
HasMergeMessageField bool `form:"-" json:"-"`
}
func (f *MergePullRequestForm) UnmarshalJSON(b []byte) error {
// 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
type aux struct {
Do1 string `json:"do"`
Do2 string `json:"Do"`
MergeTitleField1 string `json:"merge_title_field"`
MergeTitleField2 string `json:"MergeTitleField"`
MergeMessageField1 string `json:"merge_message_field"`
MergeMessageField2 string `json:"MergeMessageField"`
MergeCommitID1 string `json:"merge_commit_id"`
MergeCommitID2 string `json:"MergeCommitID"`
Do1 string `json:"do"`
Do2 string `json:"Do"`
MergeTitleField1 string `json:"merge_title_field"`
MergeTitleField2 string `json:"MergeTitleField"`
MergeMessageField1 *string `json:"merge_message_field"`
MergeMessageField2 *string `json:"MergeMessageField"`
MergeCommitID1 string `json:"merge_commit_id"`
MergeCommitID2 string `json:"MergeCommitID"`
HeadCommitID string `json:"head_commit_id"`
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.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.HeadCommitID = a.HeadCommitID
f.ForceMerge = a.ForceMerge
+15
View File
@@ -51,6 +51,7 @@ func TestMergePullRequestForm(t *testing.T) {
ForceMerge: true,
MergeWhenChecksSucceed: true,
DeleteBranchAfterMerge: new(true),
HasMergeMessageField: true,
}
t.Run("NewFields", func(t *testing.T) {
@@ -84,4 +85,18 @@ func TestMergePullRequestForm(t *testing.T) {
require.NoError(t, json.Unmarshal([]byte(input), &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
}
// 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
func GeneratePatchForUnchangedLine(ctx context.Context, gitRepo *git.Repository, commitID, treePath string, line int64, contextLines int) (string, error) {
commit, err := gitRepo.GetCommit(ctx, commitID)
+3 -2
View File
@@ -8,6 +8,7 @@ import (
issues_model "gitea.dev/models/issues"
user_model "gitea.dev/models/user"
"gitea.dev/modules/setting"
)
// 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 issues_model.CreateReaction(ctx, &issues_model.ReactionOptions{
return issues_model.CreateReaction(ctx, setting.UI.ReactionsLookup, &issues_model.ReactionOptions{
Type: content,
DoerID: doer.ID,
IssueID: issue.ID,
@@ -41,7 +42,7 @@ func CreateCommentReaction(ctx context.Context, doer *user_model.User, comment *
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,
DoerID: doer.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, ghost, issue, nil, "-1")
reactionsList, _, err := issues_model.FindReactions(t.Context(), issues_model.FindReactionsOptions{
IssueID: issue.ID,
})
reactionsList, _, err := issues_model.FindReactions(t.Context(), issues_model.FindReactionsOptions{IssueIDOnly: issue.ID})
assert.NoError(t, err)
assert.Len(t, reactionsList, 7)
_, err = reactionsList.LoadUsers(t.Context(), repo)
@@ -101,7 +99,7 @@ func TestIssueReactionCount(t *testing.T) {
reactions := reactionsList.GroupByType()
assert.Len(t, reactions["heart"], 4)
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.False(t, reactions["heart"].HasUser(5))
assert.False(t, reactions["heart"].HasUser(0))
@@ -136,10 +134,7 @@ func TestIssueCommentDeleteReaction(t *testing.T) {
addReaction(t, org3, nil, comment, "heart")
addReaction(t, user4, nil, comment, "+1")
reactionsList, _, err := issues_model.FindReactions(t.Context(), issues_model.FindReactionsOptions{
IssueID: comment.IssueID,
CommentID: comment.ID,
})
reactionsList, _, err := issues_model.FindReactions(t.Context(), issues_model.FindReactionsOptions{CommentID: comment.ID})
assert.NoError(t, err)
assert.Len(t, reactionsList, 4)
+2 -4
View File
@@ -31,6 +31,7 @@ import (
"gitea.dev/modules/timeutil"
"gitea.dev/modules/uri"
"gitea.dev/modules/util"
"gitea.dev/services/gitdiff"
"gitea.dev/services/pull"
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
comment.TreePath = util.PathJoinRel(comment.TreePath)
patch, _ := git.GetFileDiffCutAroundLine(ctx,
g.gitRepo, pr.MergeBase, headCommitID, comment.TreePath,
int64((&issues_model.Comment{Line: int64(line + comment.Position - 1)}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines,
)
patch, _ := gitdiff.GetCodeCommentPatch(ctx, g.gitRepo, pr.MergeBase, headCommitID, comment.TreePath, int64(line+comment.Position-1))
if comment.CreatedAt.IsZero() {
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)
if err != nil && !git.IsRemoteNotExistError(err) {
if err != nil {
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)
if err != nil && !git.IsRemoteNotExistError(err) {
if err != nil {
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 {
RepoID int64
Doer *user_model.User
@@ -436,8 +416,8 @@ func AddTestPullRequestTask(opts TestPullRequestOptions) {
if err = headBranchPRs.LoadAttributes(ctx); err != nil {
log.Error("PullRequestList.LoadAttributes: %v", err)
}
if invalidationErr := checkForInvalidation(ctx, headBranchPRs, opts.RepoID, opts.Doer, opts.Branch); invalidationErr != nil {
log.Error("checkForInvalidation: %v", invalidationErr)
if invalidationErr := InvalidateCodeComments(ctx, headBranchPRs, repo, opts.Branch); invalidationErr != nil {
log.Error("InvalidateCodeComments: %v", invalidationErr)
}
if err == nil {
for _, pr := range headBranchPRs {
@@ -781,9 +761,15 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ
return "", err
}
mergeMessage := strings.TrimSpace(pr.Issue.Content) // use PR's title and description as squash commit message
if setting.Repository.PullRequest.PopulateSquashCommentWithCommitMessages {
mergeMessage = formatSquashMergeCommitMessages(limitedCommits) // use PR's commit messages as squash commit message
if err := pr.LoadBaseRepo(ctx); err != nil {
return "", err
}
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)
return buildSquashMergeCommitMessages(mergeMessage, coAuthors), nil
@@ -797,7 +783,7 @@ func buildSquashMergeCommitMessages(mergeMessage string, coAuthors []string) str
msgContent, msgSep, msgTrailer := git.CommitMessageSplitTrailer(mergeMessage)
if (msgSep == "" || msgSep == "\n\n") && msgTrailer == "" {
msgContent = strings.TrimRightFunc(msgContent, unicode.IsSpace)
msgSep = "\n\n---------\n\n"
msgSep = util.Iif(msgContent == "", "", "\n\n---------\n\n")
}
var sb strings.Builder
sb.WriteString(msgContent)
+1
View File
@@ -99,6 +99,7 @@ func TestBuildSquashMergeCommitMessages(t *testing.T) {
coAuthors []string
expected string
}{
{"", []string{"the-user"}, "Co-authored-by: the-user\n"},
{"title", nil, "title"},
{"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"},
+77 -99
View File
@@ -17,14 +17,13 @@ import (
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
"gitea.dev/services/gitdiff"
notify_service "gitea.dev/services/notify"
)
func isErrBlameNotFoundOrNotEnoughLines(err error) bool {
return gitcmd.IsStderr(err, gitcmd.StderrNoSuchPath, gitcmd.StderrFileNoEnoughLines)
func isErrBlameInvalidated(err error) bool {
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.
@@ -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.
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.
// 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
commit, err := lineBlame(ctx, repo, gitRepo, branch, c.TreePath, uint(c.UnsignedLine()))
if isErrBlameNotFoundOrNotEnoughLines(err) {
commitID, err := git.LineBlameCommitID(ctx, repo, branch, c.TreePath, uint(c.UnsignedLine()))
if isErrBlameInvalidated(err) {
c.Invalidated = true
return issues_model.UpdateCommentInvalidate(ctx, c)
}
if err != nil {
return err
}
if c.CommitSHA != "" && c.CommitSHA != commit.ID.String() {
if c.CommitSHA != "" && c.CommitSHA != commitID {
c.Invalidated = true
return issues_model.UpdateCommentInvalidate(ctx, c)
}
return nil
}
// InvalidateCodeComments will lookup 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 {
// InvalidateCodeComments will look up the prs for code comments which got invalidated by change
func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestList, repo *repo_model.Repository, branch string) error {
if len(prs) == 0 {
return nil
}
@@ -97,35 +82,31 @@ func InvalidateCodeComments(ctx context.Context, prs issues_model.PullRequestLis
return fmt.Errorf("find code comments: %v", err)
}
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 nil
}
// CreateCodeComment creates a comment 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) {
var (
existsReview bool
err error
)
// CreateCodeComment() is used for:
// - Single comments
// - Comments that are part of a review
// - Comments that reply to an existing review
if !pendingReview && replyReviewID != 0 {
// CreateCodeComment creates a code comment in a review on the code line:
// When pendingReview=false:
// * no replyReviewID: create a new review and add the comment to it, and submit the review
// * replyReviewID is set: add the comment to the existing submitted review
// 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
if replyToSubmittedReview {
// 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
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
}
}
// 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 {
return nil, err
}
@@ -154,39 +135,43 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
return comment, nil
}
review, err := issues_model.GetCurrentReview(ctx, doer, issue)
if err != nil {
if !issues_model.IsErrReviewNotExist(err) {
// If no existing submitted review, create a pending review and its comment in a single transaction
// to avoid failed code comment creation leaving a pending review without any comments
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
}
if review, 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
}
}
comment, err := createCodeComment(ctx,
doer,
issue.Repo,
issue,
content,
treePath,
line,
review.ID,
attachments,
)
// add the code comment to the pending review
return createCodeComment(ctx,
doer,
issue.Repo,
issue,
content,
treePath,
line,
curReview.ID,
attachments,
)
})
if err != nil {
return nil, err
}
if !pendingReview && !existsReview {
// Submit the review we've just created so the comment shows up in the issue view
if !pendingReview {
// 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 {
return nil, err
}
@@ -214,7 +199,7 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo
defer closer.Close()
invalidated := false
head := pr.GetGitHeadRefName()
headRef := pr.GetGitHeadRefName()
if line > 0 {
if reviewID != 0 {
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,
},
})
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
invalidated = first[0].Invalidated
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 {
// otherwise, try to get the commitID from the review itself
review, err := issues_model.GetReviewByID(ctx, reviewID)
if err == nil && len(review.CommitID) > 0 {
head = review.CommitID
} else if err != nil && !issues_model.IsErrReviewNotExist(err) {
return nil, fmt.Errorf("GetReviewByID %d. Error: %w", reviewID, err)
if err != nil && !errors.Is(err, util.ErrNotExist) {
return nil, fmt.Errorf("createCodeComment: get review %d: %w", reviewID, err)
}
if review != nil && review.CommitID != "" {
headRef = review.CommitID
}
}
}
if len(commitID) == 0 {
// FIXME validate treePath
// Get latest commit referencing the commented line
// No need for get commit for base branch changes
commit, err := lineBlame(ctx, pr.BaseRepo, gitRepo, head, treePath, uint(line))
if err == nil {
commitID = commit.ID.String()
} else if !isErrBlameNotFoundOrNotEnoughLines(err) {
return nil, fmt.Errorf("LineBlame[%s, %s, %s, %d]: %w", pr.GetGitHeadRefName(), gitRepo.LogString(), treePath, line, err)
if commitID == "" {
// FIXME validate treePath (TODO: unclear comment)
// Get latest commit referencing the commented line, error can be ignored then the head commit will be used as fallback
commitID, err = git.LineBlameCommitID(ctx, pr.BaseRepo, headRef, treePath, uint(line))
// Preserve attribution to the reviewed revision, falling back to the PR head if blame fails.
if err != nil && headRef != pr.GetGitHeadRefName() {
commitID, err = git.LineBlameCommitID(ctx, pr.BaseRepo, pr.GetGitHeadRefName(), treePath, uint(line))
}
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
if len(patch) == 0 && reviewID != 0 {
if patch == "" && reviewID != 0 {
headCommitID, err := gitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName())
if err != nil {
return nil, fmt.Errorf("GetRefCommitID[%s]: %w", pr.GetGitHeadRefName(), err)
}
if len(commitID) == 0 {
if commitID == "" {
commitID = headCommitID
}
patch, err = git.GetFileDiffCutAroundLine(ctx,
gitRepo, pr.MergeBase, headCommitID, treePath,
int64((&issues_model.Comment{Line: line}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines,
)
patch, err = gitdiff.GetCodeCommentPatch(ctx, gitRepo, pr.MergeBase, headCommitID, treePath, line)
if err != nil {
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{
Type: issues_model.CommentTypeCode,
+60 -6
View File
@@ -12,6 +12,7 @@ import (
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/util"
pull_service "gitea.dev/services/pull"
"github.com/stretchr/testify/assert"
@@ -53,7 +54,6 @@ func TestDismissReview(t *testing.T) {
func TestCreateCodeCommentContextOfUnchangedLine(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 6})
require.NoError(t, pr.LoadBaseRepo(ctx))
@@ -86,22 +86,76 @@ func TestCreateCodeCommentContextOfUnchangedLine(t *testing.T) {
require.NoError(t, err)
mergeBase, err := headCommit.ParentID(0)
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()
require.NoError(t, pr.UpdateCols(ctx, "merge_base"))
for _, c := range []struct {
name string
line int64
name string
line int64
doerID int64
commitID string
}{
{"NewSide", 17},
{"OldSide", -15},
{"NewSide", 17, 2, ""},
{"OldSide", -15, 2, ""},
{"UnknownReviewCommit", 17, 4, strings.Repeat("a", 40)},
} {
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})
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)
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"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"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)
}
if _, err := db.DeleteByBean(ctx, &issues_model.Reaction{ReleaseID: rel.ID}); err != nil {
return fmt.Errorf("DeleteReleaseByID: %w", err)
}
for i := range rel.Attachments {
attachment := rel.Attachments[i]
if err := storage.Attachments.Delete(attachment.RelativePath()); err != nil {
+21
View File
@@ -8,6 +8,8 @@ import (
"testing"
"time"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
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"))
}
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) {
assert.NoError(t, unittest.PrepareTestDatabase())
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
}
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
// 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
+3
View File
@@ -10,6 +10,7 @@ import (
actions_model "gitea.dev/models/actions"
"gitea.dev/models/db"
git_model "gitea.dev/models/git"
issues_model "gitea.dev/models/issues"
"gitea.dev/models/organization"
repo_model "gitea.dev/models/repo"
"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.CommitStatusSummary{RepoID: 1, SHA: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", State: "success"},
&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})
@@ -87,4 +89,5 @@ func TestDeleteRepositoryDirectlyPurgesRepoScopedRows(t *testing.T) {
unittest.AssertNotExistsBean(t, &git_model.CommitStatusSummary{RepoID: 1})
unittest.AssertNotExistsBean(t, &git_model.CommitStatusIndex{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
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
}
}
+6 -6
View File
@@ -15,7 +15,6 @@ import (
"gitea.dev/models/unit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
"gitea.dev/modules/globallock"
"gitea.dev/modules/graceful"
"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.
// An empty defaultWikiBranch means the branch doesn't exist yet.
// return: existence, prepared file path with name, error
func prepareGitPath(ctx context.Context, gitRepo *git.Repository, defaultWikiBranch string, wikiPath WebPath) (bool, string, error) {
unescaped := string(wikiPath) + ".md"
gitPath := WebPathToGitPath(wikiPath)
if defaultWikiBranch == "" {
return false, gitPath, nil
}
// Look for both files
filesInIndex, err := gitRepo.LsTree(ctx, defaultWikiBranch, unescaped, gitPath)
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)
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 {
return err
}
@@ -152,7 +152,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
isOldWikiExist := true
oldWikiPath := newWikiPath
if oldWikiName != newWikiName {
isOldWikiExist, oldWikiPath, err = prepareGitPath(ctx, gitRepo, repo.DefaultWikiBranch, oldWikiName)
isOldWikiExist, oldWikiPath, err = prepareGitPath(ctx, gitRepo, cloneOpts.Branch, oldWikiName)
if err != nil {
return err
}
+1 -12
View File
@@ -300,18 +300,7 @@ func TestPrepareWikiFileName(t *testing.T) {
}
func TestPrepareWikiFileName_FirstPage(t *testing.T) {
// Now create a temporaryDirectory
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")
existence, newWikiPath, err := prepareGitPath(t.Context(), nil, "", "Home")
assert.False(t, existence)
assert.NoError(t, err)
assert.Equal(t, "Home.md", newWikiPath)
+2 -2
View File
@@ -28,7 +28,7 @@
</div>
</div>
<div class="ui g-modal-confirm modal" id="delete-badge-modal">
<dialog class="modal-dialog" id="delete-badge-modal">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "admin.badges.delete_badge"}}
@@ -39,6 +39,6 @@
</div>
{{template "base/modal_actions_confirm" .}}
</form>
</div>
</dialog>
{{template "admin/layout_footer" .}}
+2 -2
View File
@@ -76,7 +76,7 @@
{{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">
{{ctx.Locale.Tr "admin.emails.change_email_header"}}
</div>
@@ -95,7 +95,7 @@
{{template "base/modal_actions_confirm" .}}
</form>
</div>
</dialog>
</div>
{{template "admin/layout_footer" .}}
+2 -2
View File
@@ -61,9 +61,9 @@
{{template "base/paginate" .}}
</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="content"><pre></pre></div>
</div>
</dialog>
{{template "admin/layout_footer" .}}
+8 -6
View File
@@ -83,12 +83,14 @@
{{template "base/paginate" .}}
</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="content">
{{ctx.Locale.Tr "packages.settings.delete.notice" (HTMLFormat `<span class="%s"></span>` "package-name") (HTMLFormat `<span class="%s"></span>` "package-version")}}
</div>
{{template "base/modal_actions_confirm" .}}
</form>
<form class="form-fetch-action" method="post">
<div class="content">
{{ctx.Locale.Tr "packages.settings.delete.notice" (HTMLFormat `<span class="%s"></span>` "package-name") (HTMLFormat `<span class="%s"></span>` "package-version")}}
</div>
{{template "base/modal_actions_confirm" .}}
</form>
</dialog>
{{template "admin/layout_footer" .}}
+10 -8
View File
@@ -96,14 +96,16 @@
{{template "base/paginate" .}}
</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="content">
<p>{{ctx.Locale.Tr "repo.settings.delete_desc"}}</p>
{{ctx.Locale.Tr "repo.settings.delete_notices_2" (HTMLFormat `<span class="%s"></span>` "repo-name")}}<br>
{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}
</div>
{{template "base/modal_actions_confirm" .}}
</form>
<form class="form-fetch-action" method="post">
<div class="content">
<p>{{ctx.Locale.Tr "repo.settings.delete_desc"}}</p>
{{ctx.Locale.Tr "repo.settings.delete_notices_2" (HTMLFormat `<span class="%s"></span>` "repo-name")}}<br>
{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}
</div>
{{template "base/modal_actions_confirm" .}}
</form>
</dialog>
{{template "admin/layout_footer" .}}
+4 -4
View File
@@ -24,7 +24,7 @@
<span class="tw-flex-1"> {{svg "octicon-file-directory-fill"}} {{$dir}}</span>
<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>
<div class="ui g-modal-confirm modal" id="adopt-unadopted-modal-{{$dirI}}">
<dialog class="modal-dialog" id="adopt-unadopted-modal-{{$dirI}}">
<div class="header">
<span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting"}}</span>
</div>
@@ -38,9 +38,9 @@
<input type="hidden" name="page" value="{{$.CurrentPage}}">
{{template "base/modal_actions_confirm"}}
</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>
<div class="ui g-modal-confirm modal" id="delete-unadopted-modal-{{$dirI}}">
<dialog class="modal-dialog" id="delete-unadopted-modal-{{$dirI}}">
<div class="header">
<span class="label">{{ctx.Locale.Tr "repo.delete_preexisting"}}</span>
</div>
@@ -54,7 +54,7 @@
<input type="hidden" name="page" value="{{$.CurrentPage}}">
{{template "base/modal_actions_confirm"}}
</form>
</div>
</dialog>
</div>
</div>
{{end}}
+2 -2
View File
@@ -223,7 +223,7 @@
</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">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "settings.delete_account_title"}}
@@ -241,6 +241,6 @@
</div>
{{template "base/modal_actions_confirm" .}}
</form>
</div>
</dialog>
{{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>
{{if $userOrgs}}
<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">
{{ctx.Locale.Tr "admin.users.remove_all_orgs_title"}}
</div>
<div class="content">
<p>{{ctx.Locale.Tr "admin.users.remove_all_orgs_desc" $user.Name $userOrgsTotal}}</p>
</div>
<div class="actions">
<div class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</div>
<button class="ui red button" type="submit">{{ctx.Locale.Tr "remove_all"}}</button>
</div>
</form>
<form class="form-fetch-action" method="post" action="{{ctx.RootData.Link}}/orgs/remove-all">
<div class="content">
<p>{{ctx.Locale.Tr "admin.users.remove_all_orgs_desc" $user.Name $userOrgsTotal}}</p>
</div>
<div class="actions">
<div class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</div>
<button class="ui red button" type="submit">{{ctx.Locale.Tr "remove_all"}}</button>
</div>
</form>
</dialog>
{{end}}
</h4>
Loaded 100 of 210 files, more files were not shown because too many files have changed in this diff. Show more