Compare commits

...

8 Commits

Author SHA1 Message Date
Giteabot
15f3c1712c
Merge branch 'main' into rellist 2024-05-03 23:23:20 +08:00
wxiaoguang
0f3e717a1a
Improve grep search (#30843)
Reduce the context line number to 1, make "git grep" search respect the
include/exclude patter, and fix #30785
2024-05-03 09:13:48 +00:00
Kemal Zebari
9f0ef3621a
Don't only list code-enabled repositories when using repository API (#30817)
We should be listing all repositories by default.

Fixes #28483.
2024-05-03 15:58:31 +08:00
yp05327
a50026e2f3
Fix no edit history after editing issue's title and content (#30814)
Fix #30807

reuse functions in services
2024-05-03 14:11:51 +08:00
wxiaoguang
53b55223d1
Ignore useless error message "broken pipe" (#30801)
Fix #30792
2024-05-03 02:39:36 +00:00
silverwind
c4e875402b
Fix JS error on pull request page (#30838)
Fix this error seen on PR page, regression from
https://github.com/go-gitea/gitea/pull/30803:

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2024-05-03 10:12:10 +08:00
silverwind
b30b7df9f4
Fix body margin shifting with modals, fix error on project column edit (#30831)
Fixes: https://github.com/go-gitea/gitea/issues/30816, regression from
https://github.com/go-gitea/gitea/pull/30723.
Fixes: https://github.com/go-gitea/gitea/pull/30815, regression from
https://github.com/go-gitea/gitea/pull/30723.

Fomantic [expects a
callback](59d9b40987/src/definitions/modules/modal.js (L530-L534))
to be called during `hide` which we did not do, so it could never remove
the margin it added to `body`.

I do observe the body content shifting to right by 1px when modal opens,
but this is a bug that existed on v1.21 as well, so not a regression.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2024-05-03 01:48:24 +00:00
silverwind
c445a85528
Improve repo button row layout (#30668)
Since there is now a second `<input>` in the repo buttons, we can make a
better-looking layout with no empty space, except on mobile.

Also I fixed one bug with focus border on clone panel.

## Large

<img width="1163" alt="Screenshot 2024-04-23 at 22 25 22"
src="https://github.com/go-gitea/gitea/assets/115237/8135a572-aa67-4672-ad49-b76b06890b52">

## Medium
<img width="870" alt="Screenshot 2024-04-23 at 22 25 34"
src="https://github.com/go-gitea/gitea/assets/115237/9e93f61c-3315-4a78-8328-8cefad5b50fa">

## Mobile
<img width="416" alt="Screenshot 2024-04-23 at 22 25 52"
src="https://github.com/go-gitea/gitea/assets/115237/859e341f-807a-48e6-8bcf-31715963216c">
2024-05-02 19:10:49 +00:00
23 changed files with 248 additions and 144 deletions

View File

@ -429,62 +429,6 @@ func UpdateIssueMentions(ctx context.Context, issueID int64, mentions []*user_mo
return nil return nil
} }
// UpdateIssueByAPI updates all allowed fields of given issue.
// If the issue status is changed a statusChangeComment is returned
// similarly if the title is changed the titleChanged bool is set to true
func UpdateIssueByAPI(ctx context.Context, issue *Issue, doer *user_model.User) (statusChangeComment *Comment, titleChanged bool, err error) {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return nil, false, err
}
defer committer.Close()
if err := issue.LoadRepo(ctx); err != nil {
return nil, false, fmt.Errorf("loadRepo: %w", err)
}
// Reload the issue
currentIssue, err := GetIssueByID(ctx, issue.ID)
if err != nil {
return nil, false, err
}
if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(
"name", "content", "milestone_id", "priority",
"deadline_unix", "updated_unix", "is_locked").
Update(issue); err != nil {
return nil, false, err
}
titleChanged = currentIssue.Title != issue.Title
if titleChanged {
opts := &CreateCommentOptions{
Type: CommentTypeChangeTitle,
Doer: doer,
Repo: issue.Repo,
Issue: issue,
OldTitle: currentIssue.Title,
NewTitle: issue.Title,
}
_, err := CreateComment(ctx, opts)
if err != nil {
return nil, false, fmt.Errorf("createComment: %w", err)
}
}
if currentIssue.IsClosed != issue.IsClosed {
statusChangeComment, err = doChangeIssueStatus(ctx, issue, doer, false)
if err != nil {
return nil, false, err
}
}
if err := issue.AddCrossReferences(ctx, doer, true); err != nil {
return nil, false, err
}
return statusChangeComment, titleChanged, committer.Commit()
}
// UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it. // UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it.
func UpdateIssueDeadline(ctx context.Context, issue *Issue, deadlineUnix timeutil.TimeStamp, doer *user_model.User) (err error) { func UpdateIssueDeadline(ctx context.Context, issue *Issue, deadlineUnix timeutil.TimeStamp, doer *user_model.User) (err error) {
// if the deadline hasn't changed do nothing // if the deadline hasn't changed do nothing

View File

@ -29,6 +29,7 @@ type GrepOptions struct {
ContextLineNumber int ContextLineNumber int
IsFuzzy bool IsFuzzy bool
MaxLineLength int // the maximum length of a line to parse, exceeding chars will be truncated MaxLineLength int // the maximum length of a line to parse, exceeding chars will be truncated
PathspecList []string
} }
func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) { func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) {
@ -62,6 +63,7 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO
cmd.AddOptionValues("-e", strings.TrimLeft(search, "-")) cmd.AddOptionValues("-e", strings.TrimLeft(search, "-"))
} }
cmd.AddDynamicArguments(util.IfZero(opts.RefName, "HEAD")) cmd.AddDynamicArguments(util.IfZero(opts.RefName, "HEAD"))
cmd.AddDashesAndList(opts.PathspecList...)
opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50) opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50)
stderr := bytes.Buffer{} stderr := bytes.Buffer{}
err = cmd.Run(&RunOpts{ err = cmd.Run(&RunOpts{

View File

@ -31,6 +31,26 @@ func TestGrepSearch(t *testing.T) {
}, },
}, res) }, res)
res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{PathspecList: []string{":(glob)java-hello/*"}})
assert.NoError(t, err)
assert.Equal(t, []*GrepResult{
{
Filename: "java-hello/main.java",
LineNumbers: []int{3},
LineCodes: []string{" public static void main(String[] args)"},
},
}, res)
res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{PathspecList: []string{":(glob,exclude)java-hello/*"}})
assert.NoError(t, err)
assert.Equal(t, []*GrepResult{
{
Filename: "main.vendor.java",
LineNumbers: []int{3},
LineCodes: []string{" public static void main(String[] args)"},
},
}, res)
res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{MaxResultLimit: 1}) res, err = GrepSearch(context.Background(), repo, "void", GrepOptions{MaxResultLimit: 1})
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, []*GrepResult{ assert.Equal(t, []*GrepResult{

32
modules/setting/glob.go Normal file
View File

@ -0,0 +1,32 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import "github.com/gobwas/glob"
type GlobMatcher struct {
compiledGlob glob.Glob
patternString string
}
var _ glob.Glob = (*GlobMatcher)(nil)
func (g *GlobMatcher) Match(s string) bool {
return g.compiledGlob.Match(s)
}
func (g *GlobMatcher) PatternString() string {
return g.patternString
}
func GlobMatcherCompile(pattern string, separators ...rune) (*GlobMatcher, error) {
g, err := glob.Compile(pattern, separators...)
if err != nil {
return nil, err
}
return &GlobMatcher{
compiledGlob: g,
patternString: pattern,
}, nil
}

View File

@ -10,8 +10,6 @@ import (
"time" "time"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"github.com/gobwas/glob"
) )
// Indexer settings // Indexer settings
@ -30,8 +28,8 @@ var Indexer = struct {
RepoConnStr string RepoConnStr string
RepoIndexerName string RepoIndexerName string
MaxIndexerFileSize int64 MaxIndexerFileSize int64
IncludePatterns []glob.Glob IncludePatterns []*GlobMatcher
ExcludePatterns []glob.Glob ExcludePatterns []*GlobMatcher
ExcludeVendored bool ExcludeVendored bool
}{ }{
IssueType: "bleve", IssueType: "bleve",
@ -93,12 +91,12 @@ func loadIndexerFrom(rootCfg ConfigProvider) {
} }
// IndexerGlobFromString parses a comma separated list of patterns and returns a glob.Glob slice suited for repo indexing // IndexerGlobFromString parses a comma separated list of patterns and returns a glob.Glob slice suited for repo indexing
func IndexerGlobFromString(globstr string) []glob.Glob { func IndexerGlobFromString(globstr string) []*GlobMatcher {
extarr := make([]glob.Glob, 0, 10) extarr := make([]*GlobMatcher, 0, 10)
for _, expr := range strings.Split(strings.ToLower(globstr), ",") { for _, expr := range strings.Split(strings.ToLower(globstr), ",") {
expr = strings.TrimSpace(expr) expr = strings.TrimSpace(expr)
if expr != "" { if expr != "" {
if g, err := glob.Compile(expr, '.', '/'); err != nil { if g, err := GlobMatcherCompile(expr, '.', '/'); err != nil {
log.Info("Invalid glob expression '%s' (skipped): %v", expr, err) log.Info("Invalid glob expression '%s' (skipped): %v", expr, err)
} else { } else {
extarr = append(extarr, g) extarr = append(extarr, g)

View File

@ -85,7 +85,7 @@ type CreatePullRequestOption struct {
// EditPullRequestOption options when modify pull request // EditPullRequestOption options when modify pull request
type EditPullRequestOption struct { type EditPullRequestOption struct {
Title string `json:"title"` Title string `json:"title"`
Body string `json:"body"` Body *string `json:"body"`
Base string `json:"base"` Base string `json:"base"`
Assignee string `json:"assignee"` Assignee string `json:"assignee"`
Assignees []string `json:"assignees"` Assignees []string `json:"assignees"`

View File

@ -140,9 +140,7 @@ func serveMavenMetadata(ctx *context.Context, params parameters) {
ctx.Resp.Header().Set("Content-Length", strconv.Itoa(len(xmlMetadataWithHeader))) ctx.Resp.Header().Set("Content-Length", strconv.Itoa(len(xmlMetadataWithHeader)))
ctx.Resp.Header().Set("Content-Type", contentTypeXML) ctx.Resp.Header().Set("Content-Type", contentTypeXML)
if _, err := ctx.Resp.Write(xmlMetadataWithHeader); err != nil { _, _ = ctx.Resp.Write(xmlMetadataWithHeader)
log.Error("write bytes failed: %v", err)
}
} }
func servePackageFile(ctx *context.Context, params parameters, serveContent bool) { func servePackageFile(ctx *context.Context, params parameters, serveContent bool) {

View File

@ -29,7 +29,6 @@ import (
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/convert"
issue_service "code.gitea.io/gitea/services/issue" issue_service "code.gitea.io/gitea/services/issue"
notify_service "code.gitea.io/gitea/services/notify"
) )
// SearchIssues searches for issues across the repositories that the user has access to // SearchIssues searches for issues across the repositories that the user has access to
@ -803,12 +802,19 @@ func EditIssue(ctx *context.APIContext) {
return return
} }
oldTitle := issue.Title
if len(form.Title) > 0 { if len(form.Title) > 0 {
issue.Title = form.Title err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title)
if err != nil {
ctx.Error(http.StatusInternalServerError, "ChangeTitle", err)
return
}
} }
if form.Body != nil { if form.Body != nil {
issue.Content = *form.Body err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body)
if err != nil {
ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
return
}
} }
if form.Ref != nil { if form.Ref != nil {
err = issue_service.ChangeIssueRef(ctx, issue, ctx.Doer, *form.Ref) err = issue_service.ChangeIssueRef(ctx, issue, ctx.Doer, *form.Ref)
@ -880,24 +886,14 @@ func EditIssue(ctx *context.APIContext) {
return return
} }
} }
issue.IsClosed = api.StateClosed == api.StateType(*form.State) if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", api.StateClosed == api.StateType(*form.State)); err != nil {
} if issues_model.IsErrDependenciesLeft(err) {
statusChangeComment, titleChanged, err := issues_model.UpdateIssueByAPI(ctx, issue, ctx.Doer) ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
if err != nil { return
if issues_model.IsErrDependenciesLeft(err) { }
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies") ctx.Error(http.StatusInternalServerError, "ChangeStatus", err)
return return
} }
ctx.Error(http.StatusInternalServerError, "UpdateIssueByAPI", err)
return
}
if titleChanged {
notify_service.IssueChangeTitle(ctx, ctx.Doer, issue, oldTitle)
}
if statusChangeComment != nil {
notify_service.IssueChangeStatus(ctx, ctx.Doer, "", issue, statusChangeComment, issue.IsClosed)
} }
// Refetch from database to assign some automatic values // Refetch from database to assign some automatic values

View File

@ -602,12 +602,19 @@ func EditPullRequest(ctx *context.APIContext) {
return return
} }
oldTitle := issue.Title
if len(form.Title) > 0 { if len(form.Title) > 0 {
issue.Title = form.Title err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title)
if err != nil {
ctx.Error(http.StatusInternalServerError, "ChangeTitle", err)
return
}
} }
if len(form.Body) > 0 { if form.Body != nil {
issue.Content = form.Body err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body)
if err != nil {
ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
return
}
} }
// Update or remove deadline if set // Update or remove deadline if set
@ -686,24 +693,14 @@ func EditPullRequest(ctx *context.APIContext) {
ctx.Error(http.StatusPreconditionFailed, "MergedPRState", "cannot change state of this pull request, it was already merged") ctx.Error(http.StatusPreconditionFailed, "MergedPRState", "cannot change state of this pull request, it was already merged")
return return
} }
issue.IsClosed = api.StateClosed == api.StateType(*form.State) if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", api.StateClosed == api.StateType(*form.State)); err != nil {
} if issues_model.IsErrDependenciesLeft(err) {
statusChangeComment, titleChanged, err := issues_model.UpdateIssueByAPI(ctx, issue, ctx.Doer) ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this pull request because it still has open dependencies")
if err != nil { return
if issues_model.IsErrDependenciesLeft(err) { }
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this pull request because it still has open dependencies") ctx.Error(http.StatusInternalServerError, "ChangeStatus", err)
return return
} }
ctx.Error(http.StatusInternalServerError, "UpdateIssueByAPI", err)
return
}
if titleChanged {
notify_service.IssueChangeTitle(ctx, ctx.Doer, issue, oldTitle)
}
if statusChangeComment != nil {
notify_service.IssueChangeStatus(ctx, ctx.Doer, "", issue, statusChangeComment, issue.IsClosed)
} }
// change pull target branch // change pull target branch

View File

@ -6,10 +6,8 @@ package user
import ( import (
"net/http" "net/http"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/routers/api/v1/utils"
@ -44,7 +42,7 @@ func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
return return
} }
if ctx.IsSigned && ctx.Doer.IsAdmin || permission.UnitAccessMode(unit_model.TypeCode) >= perm.AccessModeRead { if ctx.IsSigned && ctx.Doer.IsAdmin || permission.HasAnyUnitAccess() {
apiRepos = append(apiRepos, convert.ToRepo(ctx, repos[i], permission)) apiRepos = append(apiRepos, convert.ToRepo(ctx, repos[i], permission))
} }
} }

View File

@ -17,6 +17,16 @@ import (
const tplSearch base.TplName = "repo/search" const tplSearch base.TplName = "repo/search"
func indexSettingToGitGrepPathspecList() (list []string) {
for _, expr := range setting.Indexer.IncludePatterns {
list = append(list, ":(glob)"+expr.PatternString())
}
for _, expr := range setting.Indexer.ExcludePatterns {
list = append(list, ":(glob,exclude)"+expr.PatternString())
}
return list
}
// Search render repository search page // Search render repository search page
func Search(ctx *context.Context) { func Search(ctx *context.Context) {
language := ctx.FormTrim("l") language := ctx.FormTrim("l")
@ -65,8 +75,14 @@ func Search(ctx *context.Context) {
ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx)
} }
} else { } else {
res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, git.GrepOptions{ContextLineNumber: 3, IsFuzzy: isFuzzy}) res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, keyword, git.GrepOptions{
ContextLineNumber: 1,
IsFuzzy: isFuzzy,
RefName: git.RefNameFromBranch(ctx.Repo.BranchName).String(), // BranchName should be default branch or the first existing branch
PathspecList: indexSettingToGitGrepPathspecList(),
})
if err != nil { if err != nil {
// TODO: if no branch exists, it reports: exit status 128, fatal: this operation must be run in a work tree.
ctx.ServerError("GrepSearch", err) ctx.ServerError("GrepSearch", err)
return return
} }

View File

@ -0,0 +1,19 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"testing"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
)
func TestIndexSettingToGitGrepPathspecList(t *testing.T) {
defer test.MockVariableValue(&setting.Indexer.IncludePatterns, setting.IndexerGlobFromString("a"))()
defer test.MockVariableValue(&setting.Indexer.ExcludePatterns, setting.IndexerGlobFromString("b"))()
assert.Equal(t, []string{":(glob)a", ":(glob,exclude)b"}, indexSettingToGitGrepPathspecList())
}

View File

@ -234,9 +234,7 @@ func (b *Base) plainTextInternal(skip, status int, bs []byte) {
b.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8") b.Resp.Header().Set("Content-Type", "text/plain;charset=utf-8")
b.Resp.Header().Set("X-Content-Type-Options", "nosniff") b.Resp.Header().Set("X-Content-Type-Options", "nosniff")
b.Resp.WriteHeader(status) b.Resp.WriteHeader(status)
if _, err := b.Resp.Write(bs); err != nil { _, _ = b.Resp.Write(bs)
log.ErrorWithSkip(skip, "plainTextInternal (status=%d): write bytes failed: %v", status, err)
}
} }
// PlainTextBytes renders bytes as plain text // PlainTextBytes renders bytes as plain text

View File

@ -13,6 +13,7 @@ import (
"path" "path"
"strconv" "strconv"
"strings" "strings"
"syscall"
"time" "time"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -77,7 +78,7 @@ func (ctx *Context) HTML(status int, name base.TplName) {
} }
err := ctx.Render.HTML(ctx.Resp, status, string(name), ctx.Data, ctx.TemplateContext) err := ctx.Render.HTML(ctx.Resp, status, string(name), ctx.Data, ctx.TemplateContext)
if err == nil { if err == nil || errors.Is(err, syscall.EPIPE) {
return return
} }

View File

@ -9,7 +9,7 @@
SSH SSH
</button> </button>
{{end}} {{end}}
<input id="repo-clone-url" size="20" class="js-clone-url" value="{{$.CloneButtonOriginLink.HTTPS}}" readonly> <input id="repo-clone-url" size="10" class="js-clone-url" value="{{$.CloneButtonOriginLink.HTTPS}}" readonly>
<button class="ui small icon button" id="clipboard-btn" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}" data-clipboard-target="#repo-clone-url" aria-label="{{ctx.Locale.Tr "copy_url"}}"> <button class="ui small icon button" id="clipboard-btn" data-tooltip-content="{{ctx.Locale.Tr "copy_url"}}" data-clipboard-target="#repo-clone-url" aria-label="{{ctx.Locale.Tr "copy_url"}}">
{{svg "octicon-copy" 14}} {{svg "octicon-copy" 14}}
</button> </button>

View File

@ -46,7 +46,7 @@
{{$l := Eval $n "-" 1}} {{$l := Eval $n "-" 1}}
{{$isHomepage := (eq $n 0)}} {{$isHomepage := (eq $n 0)}}
<div class="repo-button-row"> <div class="repo-button-row">
<div class="tw-flex tw-items-center tw-flex-wrap tw-gap-y-2"> <div class="repo-button-row-left">
{{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "tw-mr-1"}} {{template "repo/branch_dropdown" dict "root" . "ContainerClasses" "tw-mr-1"}}
{{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}} {{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}}
{{$cmpBranch := ""}} {{$cmpBranch := ""}}
@ -66,7 +66,7 @@
{{end}} {{end}}
{{if and .CanWriteCode .IsViewBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}} {{if and .CanWriteCode .IsViewBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}}
<button class="ui dropdown basic compact jump button tw-mr-1"{{if not .Repository.CanEnableEditor}} disabled{{end}}> <button class="ui dropdown basic compact jump button"{{if not .Repository.CanEnableEditor}} disabled{{end}}>
{{ctx.Locale.Tr "repo.editor.add_file"}} {{ctx.Locale.Tr "repo.editor.add_file"}}
{{svg "octicon-triangle-down" 14 "dropdown icon"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu"> <div class="menu">
@ -93,9 +93,9 @@
{{if $isHomepage}} {{if $isHomepage}}
{{/* only show the "code search" on the repo home page, it only does global search, {{/* only show the "code search" on the repo home page, it only does global search,
so do not show it when viewing file or directory to avoid misleading users (it doesn't search in a directory) */}} so do not show it when viewing file or directory to avoid misleading users (it doesn't search in a directory) */}}
<form class="ignore-dirty" action="{{.RepoLink}}/search" method="get"> <form class="ignore-dirty tw-flex tw-flex-1" action="{{.RepoLink}}/search" method="get">
<div class="ui small action input"> <div class="ui small action input tw-flex-1">
<input name="q" placeholder="{{ctx.Locale.Tr "search.code_kind"}}"> <input name="q" size="10" placeholder="{{ctx.Locale.Tr "search.code_kind"}}">
{{template "shared/search/button"}} {{template "shared/search/button"}}
</div> </div>
</form> </form>
@ -113,7 +113,7 @@
</span> </span>
{{end}} {{end}}
</div> </div>
<div class="tw-flex tw-items-center"> <div class="repo-button-row-right">
<!-- Only show clone panel in repository home page --> <!-- Only show clone panel in repository home page -->
{{if $isHomepage}} {{if $isHomepage}}
<div class="clone-panel ui action tiny input"> <div class="clone-panel ui action tiny input">

View File

@ -194,6 +194,10 @@ func TestAPIEditIssue(t *testing.T) {
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10}) issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
repoAfter := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID}) repoAfter := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
// check comment history
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: issueAfter.ID, OldTitle: issueBefore.Title, NewTitle: title})
unittest.AssertExistsAndLoadBean(t, &issues_model.ContentHistory{IssueID: issueAfter.ID, ContentText: body, IsFirstCreated: false})
// check deleted user // check deleted user
assert.Equal(t, int64(500), issueAfter.PosterID) assert.Equal(t, int64(500), issueAfter.PosterID)
assert.NoError(t, issueAfter.LoadAttributes(db.DefaultContext)) assert.NoError(t, issueAfter.LoadAttributes(db.DefaultContext))

View File

@ -223,23 +223,33 @@ func TestAPIEditPull(t *testing.T) {
session := loginUser(t, owner10.Name) session := loginUser(t, owner10.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
title := "create a success pr"
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &api.CreatePullRequestOption{ req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &api.CreatePullRequestOption{
Head: "develop", Head: "develop",
Base: "master", Base: "master",
Title: "create a success pr", Title: title,
}).AddTokenAuth(token) }).AddTokenAuth(token)
pull := new(api.PullRequest) apiPull := new(api.PullRequest)
resp := MakeRequest(t, req, http.StatusCreated) resp := MakeRequest(t, req, http.StatusCreated)
DecodeJSON(t, resp, pull) DecodeJSON(t, resp, apiPull)
assert.EqualValues(t, "master", pull.Base.Name) assert.EqualValues(t, "master", apiPull.Base.Name)
req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, pull.Index), &api.EditPullRequestOption{ newTitle := "edit a this pr"
newBody := "edited body"
req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, apiPull.Index), &api.EditPullRequestOption{
Base: "feature/1", Base: "feature/1",
Title: "edit a this pr", Title: newTitle,
Body: &newBody,
}).AddTokenAuth(token) }).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusCreated) resp = MakeRequest(t, req, http.StatusCreated)
DecodeJSON(t, resp, pull) DecodeJSON(t, resp, apiPull)
assert.EqualValues(t, "feature/1", pull.Base.Name) assert.EqualValues(t, "feature/1", apiPull.Base.Name)
// check comment history
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
err := pull.LoadIssue(db.DefaultContext)
assert.NoError(t, err)
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: pull.Issue.ID, OldTitle: title, NewTitle: newTitle})
unittest.AssertExistsAndLoadBean(t, &issues_model.ContentHistory{IssueID: pull.Issue.ID, ContentText: newBody, IsFirstCreated: false})
req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, pull.Index), &api.EditPullRequestOption{ req = NewRequestWithJSON(t, http.MethodPatch, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner10.Name, repo10.Name, pull.Index), &api.EditPullRequestOption{
Base: "not-exist", Base: "not-exist",

View File

@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -326,6 +327,39 @@ func TestAPIOrgRepos(t *testing.T) {
} }
} }
// See issue #28483. Tests to make sure we consider more than just code unit-enabled repositories.
func TestAPIOrgReposWithCodeUnitDisabled(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: "repo21"})
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo21.OwnerID})
// Disable code repository unit.
var units []unit_model.Type
units = append(units, unit_model.TypeCode)
if err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repo21, nil, units); err != nil {
assert.Fail(t, "should have been able to delete code repository unit; failed to %v", err)
}
assert.False(t, repo21.UnitEnabled(db.DefaultContext, unit_model.TypeCode))
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrganization)
req := NewRequestf(t, "GET", "/api/v1/orgs/%s/repos", org3.Name).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var apiRepos []*api.Repository
DecodeJSON(t, resp, &apiRepos)
var repoNames []string
for _, r := range apiRepos {
repoNames = append(repoNames, r.Name)
}
assert.Contains(t, repoNames, repo21.Name)
}
func TestAPIGetRepoByIDUnauthorized(t *testing.T) { func TestAPIGetRepoByIDUnauthorized(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})

View File

@ -188,8 +188,8 @@
.ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection:hover, .ui.action.input:not([class*="left action"]) > input:focus + .ui.dropdown.selection:hover,
.ui.action.input:not([class*="left action"]) > input:focus + .button, .ui.action.input:not([class*="left action"]) > input:focus + .button,
.ui.action.input:not([class*="left action"]) > input:focus + .button:hover, .ui.action.input:not([class*="left action"]) > input:focus + .button:hover,
.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button, .ui.action.input:not([class*="left action"]) > input:focus + i.icon + .button,
.ui.action.input:not([class*="left action"]) > input:focus + .icon + .button:hover { .ui.action.input:not([class*="left action"]) > input:focus + i.icon + .button:hover {
border-left-color: var(--color-primary); border-left-color: var(--color-primary);
} }
.ui.action.input:not([class*="left action"]) > input:focus { .ui.action.input:not([class*="left action"]) > input:focus {

View File

@ -128,15 +128,22 @@
margin-bottom: 12px; margin-bottom: 12px;
} }
.repository .clone-panel #repo-clone-url { .repository .clone-panel {
width: 320px; display: flex;
border-radius: 0; flex: 1;
} }
@media (max-width: 991.98px) { .repository.wiki .clone-panel {
.repository .clone-panel #repo-clone-url { flex: 0;
width: 200px; }
}
.repository.wiki .clone-panel input {
width: 20ch;
}
.repository .clone-panel #repo-clone-url {
border-radius: 0;
flex: 1;
} }
.repository .ui.action.input.clone-panel > button + button, .repository .ui.action.input.clone-panel > button + button,
@ -2229,17 +2236,37 @@ td .commit-summary {
} }
.repo-button-row { .repo-button-row {
margin: 10px 0; margin: 8px 0;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5em; gap: 8px;
flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
} }
.repo-button-row-left,
.repo-button-row-right {
display: flex;
flex: 1;
align-items: center;
gap: 0.5rem;
}
.repo-button-row-right {
justify-content: flex-end;
}
@media (max-width: 991px) {
.repository:not(.wiki) .repo-button-row {
flex-direction: column;
align-items: stretch;
}
}
.repo-button-row .button { .repo-button-row .button {
padding: 6px 10px !important; padding: 6px 10px !important;
height: 30px; height: 30px;
flex-shrink: 0;
margin: 0;
} }
.repo-button-row .button.dropdown:not(.icon) { .repo-button-row .button.dropdown:not(.icon) {
@ -2250,6 +2277,12 @@ td .commit-summary {
height: 30px; height: 30px;
} }
@media (max-width: 600px) {
.repo-button-row-left {
flex-wrap: wrap;
}
}
tbody.commit-list { tbody.commit-list {
vertical-align: baseline; vertical-align: baseline;
} }

View File

@ -57,6 +57,7 @@ export function initRepoCommentForm() {
function initBranchSelector() { function initBranchSelector() {
const elSelectBranch = document.querySelector('.ui.dropdown.select-branch'); const elSelectBranch = document.querySelector('.ui.dropdown.select-branch');
if (!elSelectBranch) return;
const isForNewIssue = elSelectBranch.getAttribute('data-for-new-issue') === 'true'; const isForNewIssue = elSelectBranch.getAttribute('data-for-new-issue') === 'true';
const $selectBranch = $(elSelectBranch); const $selectBranch = $(elSelectBranch);

View File

@ -3,11 +3,12 @@ import {queryElemChildren} from '../../utils/dom.js';
export function initFomanticDimmer() { export function initFomanticDimmer() {
// stand-in for removed dimmer module // stand-in for removed dimmer module
$.fn.dimmer = function (arg0, $el) { $.fn.dimmer = function (arg0, arg1) {
if (arg0 === 'add content') { if (arg0 === 'add content') {
const $el = arg1;
const existingDimmer = document.querySelector('body > .ui.dimmer'); const existingDimmer = document.querySelector('body > .ui.dimmer');
if (existingDimmer) { if (existingDimmer) {
queryElemChildren(existingDimmer, '*', (el) => el.remove()); queryElemChildren(existingDimmer, '*', (el) => el.classList.add('hidden'));
this._dimmer = existingDimmer; this._dimmer = existingDimmer;
} else { } else {
this._dimmer = document.createElement('div'); this._dimmer = document.createElement('div');
@ -21,8 +22,10 @@ export function initFomanticDimmer() {
this._dimmer.classList.add('active'); this._dimmer.classList.add('active');
document.body.classList.add('tw-overflow-hidden'); document.body.classList.add('tw-overflow-hidden');
} else if (arg0 === 'hide') { } else if (arg0 === 'hide') {
const cb = arg1;
this._dimmer.classList.remove('active'); this._dimmer.classList.remove('active');
document.body.classList.remove('tw-overflow-hidden'); document.body.classList.remove('tw-overflow-hidden');
cb();
} }
return this; return this;
}; };