@@ -1,7 +1,7 @@ | |||
# Gitea - Docker | |||
Dockerfile is found in root of repository. | |||
Dockerfile is found in the root of the repository. | |||
Docker image can be found on [docker hub](https://hub.docker.com/r/gitea/gitea) | |||
Docker image can be found on [docker hub](https://hub.docker.com/r/gitea/gitea). | |||
Documentation on using docker image can be found on [Gitea Docs site](https://docs.gitea.com/installation/install-with-docker-rootless) | |||
Documentation on using docker image can be found on [Gitea Docs site](https://docs.gitea.com/installation/install-with-docker-rootless). |
@@ -429,62 +429,6 @@ func UpdateIssueMentions(ctx context.Context, issueID int64, mentions []*user_mo | |||
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. | |||
func UpdateIssueDeadline(ctx context.Context, issue *Issue, deadlineUnix timeutil.TimeStamp, doer *user_model.User) (err error) { | |||
// if the deadline hasn't changed do nothing |
@@ -29,6 +29,7 @@ type GrepOptions struct { | |||
ContextLineNumber int | |||
IsFuzzy bool | |||
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) { | |||
@@ -62,6 +63,7 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO | |||
cmd.AddOptionValues("-e", strings.TrimLeft(search, "-")) | |||
} | |||
cmd.AddDynamicArguments(util.IfZero(opts.RefName, "HEAD")) | |||
cmd.AddDashesAndList(opts.PathspecList...) | |||
opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50) | |||
stderr := bytes.Buffer{} | |||
err = cmd.Run(&RunOpts{ |
@@ -31,6 +31,26 @@ func TestGrepSearch(t *testing.T) { | |||
}, | |||
}, 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}) | |||
assert.NoError(t, err) | |||
assert.Equal(t, []*GrepResult{ |
@@ -10,6 +10,7 @@ import ( | |||
"path" | |||
"path/filepath" | |||
"regexp" | |||
"slices" | |||
"strings" | |||
"sync" | |||
@@ -54,7 +55,7 @@ var ( | |||
shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`) | |||
// anyHashPattern splits url containing SHA into parts | |||
anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~_%.a-zA-Z0-9/]+)?(#[-+~_%.a-zA-Z0-9]+)?`) | |||
anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~%./\w]+)?(\?[-+~%.\w&=]+)?(#[-+~%.\w]+)?`) | |||
// comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash" | |||
comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`) | |||
@@ -591,7 +592,8 @@ func replaceContentList(node *html.Node, i, j int, newNodes []*html.Node) { | |||
func mentionProcessor(ctx *RenderContext, node *html.Node) { | |||
start := 0 | |||
for node != nil { | |||
nodeStop := node.NextSibling | |||
for node != nodeStop { | |||
found, loc := references.FindFirstMentionBytes(util.UnsafeStringToBytes(node.Data[start:])) | |||
if !found { | |||
node = node.NextSibling | |||
@@ -962,57 +964,68 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) { | |||
} | |||
} | |||
// fullHashPatternProcessor renders SHA containing URLs | |||
func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) { | |||
if ctx.Metas == nil { | |||
return | |||
type anyHashPatternResult struct { | |||
PosStart int | |||
PosEnd int | |||
FullURL string | |||
CommitID string | |||
SubPath string | |||
QueryHash string | |||
} | |||
func anyHashPatternExtract(s string) (ret anyHashPatternResult, ok bool) { | |||
m := anyHashPattern.FindStringSubmatchIndex(s) | |||
if m == nil { | |||
return ret, false | |||
} | |||
next := node.NextSibling | |||
for node != nil && node != next { | |||
m := anyHashPattern.FindStringSubmatchIndex(node.Data) | |||
if m == nil { | |||
return | |||
ret.PosStart, ret.PosEnd = m[0], m[1] | |||
ret.FullURL = s[ret.PosStart:ret.PosEnd] | |||
if strings.HasSuffix(ret.FullURL, ".") { | |||
// if url ends in '.', it's very likely that it is not part of the actual url but used to finish a sentence. | |||
ret.PosEnd-- | |||
ret.FullURL = ret.FullURL[:len(ret.FullURL)-1] | |||
for i := 0; i < len(m); i++ { | |||
m[i] = min(m[i], ret.PosEnd) | |||
} | |||
} | |||
urlFull := node.Data[m[0]:m[1]] | |||
text := base.ShortSha(node.Data[m[2]:m[3]]) | |||
ret.CommitID = s[m[2]:m[3]] | |||
if m[5] > 0 { | |||
ret.SubPath = s[m[4]:m[5]] | |||
} | |||
// 3rd capture group matches a optional path | |||
subpath := "" | |||
if m[5] > 0 { | |||
subpath = node.Data[m[4]:m[5]] | |||
} | |||
lastStart, lastEnd := m[len(m)-2], m[len(m)-1] | |||
if lastEnd > 0 { | |||
ret.QueryHash = s[lastStart:lastEnd][1:] | |||
} | |||
return ret, true | |||
} | |||
// 4th capture group matches a optional url hash | |||
hash := "" | |||
if m[7] > 0 { | |||
hash = node.Data[m[6]:m[7]][1:] | |||
// fullHashPatternProcessor renders SHA containing URLs | |||
func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) { | |||
if ctx.Metas == nil { | |||
return | |||
} | |||
nodeStop := node.NextSibling | |||
for node != nodeStop { | |||
if node.Type != html.TextNode { | |||
node = node.NextSibling | |||
continue | |||
} | |||
start := m[0] | |||
end := m[1] | |||
// If url ends in '.', it's very likely that it is not part of the | |||
// actual url but used to finish a sentence. | |||
if strings.HasSuffix(urlFull, ".") { | |||
end-- | |||
urlFull = urlFull[:len(urlFull)-1] | |||
if hash != "" { | |||
hash = hash[:len(hash)-1] | |||
} else if subpath != "" { | |||
subpath = subpath[:len(subpath)-1] | |||
} | |||
ret, ok := anyHashPatternExtract(node.Data) | |||
if !ok { | |||
node = node.NextSibling | |||
continue | |||
} | |||
if subpath != "" { | |||
text += subpath | |||
text := base.ShortSha(ret.CommitID) | |||
if ret.SubPath != "" { | |||
text += ret.SubPath | |||
} | |||
if hash != "" { | |||
text += " (" + hash + ")" | |||
if ret.QueryHash != "" { | |||
text += " (" + ret.QueryHash + ")" | |||
} | |||
replaceContent(node, start, end, createCodeLink(urlFull, text, "commit")) | |||
replaceContent(node, ret.PosStart, ret.PosEnd, createCodeLink(ret.FullURL, text, "commit")) | |||
node = node.NextSibling.NextSibling | |||
} | |||
} | |||
@@ -1021,19 +1034,16 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) { | |||
if ctx.Metas == nil { | |||
return | |||
} | |||
next := node.NextSibling | |||
for node != nil && node != next { | |||
m := comparePattern.FindStringSubmatchIndex(node.Data) | |||
if m == nil { | |||
return | |||
nodeStop := node.NextSibling | |||
for node != nodeStop { | |||
if node.Type != html.TextNode { | |||
node = node.NextSibling | |||
continue | |||
} | |||
// Ensure that every group (m[0]...m[7]) has a match | |||
for i := 0; i < 8; i++ { | |||
if m[i] == -1 { | |||
return | |||
} | |||
m := comparePattern.FindStringSubmatchIndex(node.Data) | |||
if m == nil || slices.Contains(m[:8], -1) { // ensure that every group (m[0]...m[7]) has a match | |||
node = node.NextSibling | |||
continue | |||
} | |||
urlFull := node.Data[m[0]:m[1]] |
@@ -60,7 +60,8 @@ func renderCodeBlock(ctx *RenderContext, node *html.Node) (urlPosStart, urlPosSt | |||
} | |||
func codePreviewPatternProcessor(ctx *RenderContext, node *html.Node) { | |||
for node != nil { | |||
nodeStop := node.NextSibling | |||
for node != nodeStop { | |||
if node.Type != html.TextNode { | |||
node = node.NextSibling | |||
continue |
@@ -399,36 +399,61 @@ func TestRegExp_sha1CurrentPattern(t *testing.T) { | |||
} | |||
func TestRegExp_anySHA1Pattern(t *testing.T) { | |||
testCases := map[string][]string{ | |||
testCases := map[string]anyHashPatternResult{ | |||
"https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js#L2703": { | |||
"a644101ed04d0beacea864ce805e0c4f86ba1cd1", | |||
"/test/unit/event.js", | |||
"#L2703", | |||
CommitID: "a644101ed04d0beacea864ce805e0c4f86ba1cd1", | |||
SubPath: "/test/unit/event.js", | |||
QueryHash: "L2703", | |||
}, | |||
"https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js": { | |||
"a644101ed04d0beacea864ce805e0c4f86ba1cd1", | |||
"/test/unit/event.js", | |||
"", | |||
CommitID: "a644101ed04d0beacea864ce805e0c4f86ba1cd1", | |||
SubPath: "/test/unit/event.js", | |||
}, | |||
"https://github.com/jquery/jquery/commit/0705be475092aede1eddae01319ec931fb9c65fc": { | |||
"0705be475092aede1eddae01319ec931fb9c65fc", | |||
"", | |||
"", | |||
CommitID: "0705be475092aede1eddae01319ec931fb9c65fc", | |||
}, | |||
"https://github.com/jquery/jquery/tree/0705be475092aede1eddae01319ec931fb9c65fc/src": { | |||
"0705be475092aede1eddae01319ec931fb9c65fc", | |||
"/src", | |||
"", | |||
CommitID: "0705be475092aede1eddae01319ec931fb9c65fc", | |||
SubPath: "/src", | |||
}, | |||
"https://try.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2": { | |||
"d8a994ef243349f321568f9e36d5c3f444b99cae", | |||
"", | |||
"#diff-2", | |||
CommitID: "d8a994ef243349f321568f9e36d5c3f444b99cae", | |||
QueryHash: "diff-2", | |||
}, | |||
"non-url": {}, | |||
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678?a=b#L1-L2": { | |||
CommitID: "1234567812345678123456781234567812345678123456781234567812345678", | |||
QueryHash: "L1-L2", | |||
}, | |||
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678.": { | |||
CommitID: "1234567812345678123456781234567812345678123456781234567812345678", | |||
}, | |||
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678/sub.": { | |||
CommitID: "1234567812345678123456781234567812345678123456781234567812345678", | |||
SubPath: "/sub", | |||
}, | |||
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678?a=b.": { | |||
CommitID: "1234567812345678123456781234567812345678123456781234567812345678", | |||
}, | |||
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678?a=b&c=d": { | |||
CommitID: "1234567812345678123456781234567812345678123456781234567812345678", | |||
}, | |||
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678#hash.": { | |||
CommitID: "1234567812345678123456781234567812345678123456781234567812345678", | |||
QueryHash: "hash", | |||
}, | |||
} | |||
for k, v := range testCases { | |||
assert.Equal(t, anyHashPattern.FindStringSubmatch(k)[1:], v) | |||
ret, ok := anyHashPatternExtract(k) | |||
if v.CommitID == "" { | |||
assert.False(t, ok) | |||
} else { | |||
assert.EqualValues(t, strings.TrimSuffix(k, "."), ret.FullURL) | |||
assert.EqualValues(t, v.CommitID, ret.CommitID) | |||
assert.EqualValues(t, v.SubPath, ret.SubPath) | |||
assert.EqualValues(t, v.QueryHash, ret.QueryHash) | |||
} | |||
} | |||
} | |||
@@ -124,6 +124,11 @@ func TestRender_CrossReferences(t *testing.T) { | |||
test( | |||
util.URLJoin(markup.TestAppURL, "gogitea", "some-repo-name", "issues", "12345"), | |||
`<p><a href="`+util.URLJoin(markup.TestAppURL, "gogitea", "some-repo-name", "issues", "12345")+`" class="ref-issue" rel="nofollow">gogitea/some-repo-name#12345</a></p>`) | |||
inputURL := "https://host/a/b/commit/0123456789012345678901234567890123456789/foo.txt?a=b#L2-L3" | |||
test( | |||
inputURL, | |||
`<p><a href="`+inputURL+`" rel="nofollow"><code>0123456789/foo.txt (L2-L3)</code></a></p>`) | |||
} | |||
func TestMisc_IsSameDomain(t *testing.T) { | |||
@@ -695,7 +700,7 @@ func TestIssue18471(t *testing.T) { | |||
}, strings.NewReader(data), &res) | |||
assert.NoError(t, err) | |||
assert.Equal(t, "<a href=\"http://domain/org/repo/compare/783b039...da951ce\" class=\"compare\"><code class=\"nohighlight\">783b039...da951ce</code></a>", res.String()) | |||
assert.Equal(t, `<a href="http://domain/org/repo/compare/783b039...da951ce" class="compare"><code class="nohighlight">783b039...da951ce</code></a>`, res.String()) | |||
} | |||
func TestIsFullURL(t *testing.T) { |
@@ -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 | |||
} |
@@ -10,8 +10,6 @@ import ( | |||
"time" | |||
"code.gitea.io/gitea/modules/log" | |||
"github.com/gobwas/glob" | |||
) | |||
// Indexer settings | |||
@@ -30,8 +28,8 @@ var Indexer = struct { | |||
RepoConnStr string | |||
RepoIndexerName string | |||
MaxIndexerFileSize int64 | |||
IncludePatterns []glob.Glob | |||
ExcludePatterns []glob.Glob | |||
IncludePatterns []*GlobMatcher | |||
ExcludePatterns []*GlobMatcher | |||
ExcludeVendored bool | |||
}{ | |||
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 | |||
func IndexerGlobFromString(globstr string) []glob.Glob { | |||
extarr := make([]glob.Glob, 0, 10) | |||
func IndexerGlobFromString(globstr string) []*GlobMatcher { | |||
extarr := make([]*GlobMatcher, 0, 10) | |||
for _, expr := range strings.Split(strings.ToLower(globstr), ",") { | |||
expr = strings.TrimSpace(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) | |||
} else { | |||
extarr = append(extarr, g) |
@@ -85,7 +85,7 @@ type CreatePullRequestOption struct { | |||
// EditPullRequestOption options when modify pull request | |||
type EditPullRequestOption struct { | |||
Title string `json:"title"` | |||
Body string `json:"body"` | |||
Body *string `json:"body"` | |||
Base string `json:"base"` | |||
Assignee string `json:"assignee"` | |||
Assignees []string `json:"assignees"` |
@@ -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-Type", contentTypeXML) | |||
if _, err := ctx.Resp.Write(xmlMetadataWithHeader); err != nil { | |||
log.Error("write bytes failed: %v", err) | |||
} | |||
_, _ = ctx.Resp.Write(xmlMetadataWithHeader) | |||
} | |||
func servePackageFile(ctx *context.Context, params parameters, serveContent bool) { |
@@ -29,7 +29,6 @@ import ( | |||
"code.gitea.io/gitea/services/context" | |||
"code.gitea.io/gitea/services/convert" | |||
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 | |||
@@ -803,12 +802,19 @@ func EditIssue(ctx *context.APIContext) { | |||
return | |||
} | |||
oldTitle := issue.Title | |||
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 { | |||
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 { | |||
err = issue_service.ChangeIssueRef(ctx, issue, ctx.Doer, *form.Ref) | |||
@@ -880,24 +886,14 @@ func EditIssue(ctx *context.APIContext) { | |||
return | |||
} | |||
} | |||
issue.IsClosed = api.StateClosed == api.StateType(*form.State) | |||
} | |||
statusChangeComment, titleChanged, err := issues_model.UpdateIssueByAPI(ctx, issue, ctx.Doer) | |||
if err != nil { | |||
if issues_model.IsErrDependenciesLeft(err) { | |||
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies") | |||
if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", api.StateClosed == api.StateType(*form.State)); err != nil { | |||
if issues_model.IsErrDependenciesLeft(err) { | |||
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies") | |||
return | |||
} | |||
ctx.Error(http.StatusInternalServerError, "ChangeStatus", err) | |||
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 |
@@ -602,12 +602,19 @@ func EditPullRequest(ctx *context.APIContext) { | |||
return | |||
} | |||
oldTitle := issue.Title | |||
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 { | |||
issue.Content = form.Body | |||
if form.Body != nil { | |||
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 | |||
@@ -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") | |||
return | |||
} | |||
issue.IsClosed = api.StateClosed == api.StateType(*form.State) | |||
} | |||
statusChangeComment, titleChanged, err := issues_model.UpdateIssueByAPI(ctx, issue, ctx.Doer) | |||
if err != nil { | |||
if issues_model.IsErrDependenciesLeft(err) { | |||
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this pull request because it still has open dependencies") | |||
if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", api.StateClosed == api.StateType(*form.State)); err != nil { | |||
if issues_model.IsErrDependenciesLeft(err) { | |||
ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this pull request because it still has open dependencies") | |||
return | |||
} | |||
ctx.Error(http.StatusInternalServerError, "ChangeStatus", err) | |||
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 |
@@ -6,10 +6,8 @@ package user | |||
import ( | |||
"net/http" | |||
"code.gitea.io/gitea/models/perm" | |||
access_model "code.gitea.io/gitea/models/perm/access" | |||
repo_model "code.gitea.io/gitea/models/repo" | |||
unit_model "code.gitea.io/gitea/models/unit" | |||
user_model "code.gitea.io/gitea/models/user" | |||
api "code.gitea.io/gitea/modules/structs" | |||
"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) | |||
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)) | |||
} | |||
} |
@@ -17,6 +17,16 @@ import ( | |||
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 | |||
func Search(ctx *context.Context) { | |||
language := ctx.FormTrim("l") | |||
@@ -65,8 +75,14 @@ func Search(ctx *context.Context) { | |||
ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) | |||
} | |||
} 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 { | |||
// TODO: if no branch exists, it reports: exit status 128, fatal: this operation must be run in a work tree. | |||
ctx.ServerError("GrepSearch", err) | |||
return | |||
} |
@@ -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()) | |||
} |
@@ -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("X-Content-Type-Options", "nosniff") | |||
b.Resp.WriteHeader(status) | |||
if _, err := b.Resp.Write(bs); err != nil { | |||
log.ErrorWithSkip(skip, "plainTextInternal (status=%d): write bytes failed: %v", status, err) | |||
} | |||
_, _ = b.Resp.Write(bs) | |||
} | |||
// PlainTextBytes renders bytes as plain text |
@@ -13,6 +13,7 @@ import ( | |||
"path" | |||
"strconv" | |||
"strings" | |||
"syscall" | |||
"time" | |||
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) | |||
if err == nil { | |||
if err == nil || errors.Is(err, syscall.EPIPE) { | |||
return | |||
} | |||
@@ -0,0 +1,109 @@ | |||
{{template "base/head" .}} | |||
<link rel="stylesheet" href="{{AssetUrlPrefix}}/css/devtest.css?v={{AssetVersion}}"> | |||
<div class="page-content devtest ui container"> | |||
<div> | |||
<h2>Dropdown</h2> | |||
<div> | |||
<div class="ui dropdown tw-border tw-border-red tw-border-dashed" data-tooltip-content="border for demo purpose only"> | |||
<span class="text">search-input & flex-item in menu</span> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
<div class="menu flex-items-menu"> | |||
<div class="ui icon search input"><i class="icon">{{svg "octicon-search"}}</i><input type="text" value="search input in menu"></div> | |||
<div class="item"><input type="radio">item</div> | |||
<div class="item"><input type="radio">item</div> | |||
</div> | |||
</div> | |||
<div class="ui search selection dropdown"> | |||
<span class="text">search ...</span> | |||
<input name="value" class="search"> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
{{svg "octicon-x" 14 "remove icon"}} | |||
<div class="menu"> | |||
<div class="item">item</div> | |||
</div> | |||
</div> | |||
<div class="ui multiple selection dropdown"> | |||
<input class="hidden" value="1"> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
{{svg "octicon-x" 14 "remove icon"}} | |||
<div class="default text">empty multiple dropdown</div> | |||
<div class="menu"> | |||
<div class="item">item</div> | |||
</div> | |||
</div> | |||
<div class="ui multiple clearable search selection dropdown"> | |||
<input type="hidden" value="1"> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
{{svg "octicon-x" 14 "remove icon"}} | |||
<div class="default text">clearable search dropdown</div> | |||
<div class="menu"> | |||
<div class="item" data-value="1">item</div> | |||
</div> | |||
</div> | |||
<div class="ui buttons"> | |||
<button class="ui button">Button with Dropdown</button> | |||
<div class="ui dropdown button icon"> | |||
{{svg "octicon-triangle-down"}} | |||
<div class="menu"> | |||
<div class="item">item</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<h2>Selection</h2> | |||
<div> | |||
{{/* the "selection" class is optional, it will be added by JS automatically */}} | |||
<select class="ui dropdown selection ellipsis-items-nowrap"> | |||
<option>a</option> | |||
<option>abcdefuvwxyz</option> | |||
<option>loooooooooooooooooooooooooooooooooooooooooooooooooooooooooong</option> | |||
</select> | |||
<select class="ui dropdown ellipsis-items-nowrap tw-max-w-[8em]"> | |||
<option>loooooooooooooooooooooooooooooooooooooooooooooooooooooooooong</option> | |||
<option>abcdefuvwxyz</option> | |||
<option>a</option> | |||
</select> | |||
</div> | |||
<h2>Dropdown Button (demo only without menu)</h2> | |||
<div> | |||
<div class="ui dropdown mini button"> | |||
<span class="text">mini dropdown</span> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
</div> | |||
<div class="ui dropdown tiny button"> | |||
<span class="text">tiny dropdown</span> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
</div> | |||
<div class="ui button dropdown"> | |||
<span class="text">button dropdown</span> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
</div> | |||
</div> | |||
<div> | |||
<div class="ui dropdown mini compact button"> | |||
<span class="text">mini compact</span> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
</div> | |||
<div class="ui dropdown tiny compact button"> | |||
<span class="text">tiny compact</span> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
</div> | |||
<div class="ui button compact dropdown"> | |||
<span class="text">button compact</span> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
</div> | |||
</div> | |||
<div> | |||
<hr> | |||
<div class="ui tiny button">Other button align with ...</div> | |||
<div class="ui dropdown tiny button"> | |||
<span class="text">... Dropdown Button</span> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
{{template "base/footer" .}} |
@@ -180,94 +180,6 @@ | |||
<input type="text" placeholder="place holder"> | |||
</div> | |||
</div> | |||
<h2>Dropdown with SVG</h2> | |||
<div> | |||
<div class="ui dropdown tw-border tw-border-red tw-border-dashed" data-tooltip-content="border for demo purpose only"> | |||
<span class="text">search-input & flex-item in menu</span> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
<div class="menu flex-items-menu"> | |||
<div class="ui icon search input"><i class="icon">{{svg "octicon-search"}}</i><input type="text" value="search input in menu"></div> | |||
<div class="item"><input type="radio">item</div> | |||
<div class="item"><input type="radio">item</div> | |||
</div> | |||
</div> | |||
<div class="ui search selection dropdown"> | |||
<span class="text">search ...</span> | |||
<input name="value" class="search"> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
{{svg "octicon-x" 14 "remove icon"}} | |||
<div class="menu"> | |||
<div class="item">item</div> | |||
</div> | |||
</div> | |||
<div class="ui multiple selection dropdown"> | |||
<input class="hidden" value="1"> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
{{svg "octicon-x" 14 "remove icon"}} | |||
<div class="default text">empty multiple dropdown</div> | |||
<div class="menu"> | |||
<div class="item">item</div> | |||
</div> | |||
</div> | |||
<div class="ui multiple clearable search selection dropdown"> | |||
<input type="hidden" value="1"> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
{{svg "octicon-x" 14 "remove icon"}} | |||
<div class="default text">clearable search dropdown</div> | |||
<div class="menu"> | |||
<div class="item" data-value="1">item</div> | |||
</div> | |||
</div> | |||
<div class="ui buttons"> | |||
<button class="ui button">Button with Dropdown</button> | |||
<div class="ui dropdown button icon"> | |||
{{svg "octicon-triangle-down"}} | |||
<div class="menu"> | |||
<div class="item">item</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<div> | |||
<div class="ui dropdown mini button"> | |||
<span class="text">mini dropdown</span> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
</div> | |||
<div class="ui dropdown tiny button"> | |||
<span class="text">tiny dropdown</span> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
</div> | |||
<div class="ui button dropdown"> | |||
<span class="text">button dropdown</span> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
</div> | |||
</div> | |||
<div> | |||
<div class="ui dropdown mini compact button"> | |||
<span class="text">mini compact</span> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
</div> | |||
<div class="ui dropdown tiny compact button"> | |||
<span class="text">tiny compact</span> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
</div> | |||
<div class="ui button compact dropdown"> | |||
<span class="text">button compact</span> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
</div> | |||
</div> | |||
<div> | |||
<hr> | |||
<div class="ui tiny button">Button align with ...</div> | |||
<div class="ui dropdown tiny button"> | |||
<span class="text">... Dropdown Button</span> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
</div> | |||
</div> | |||
</div> | |||
<div> |
@@ -157,168 +157,171 @@ | |||
<!-- Optional Settings --> | |||
<h4 class="ui dividing header">{{ctx.Locale.Tr "install.optional_title"}}</h4> | |||
<!-- Email --> | |||
<details class="optional field"> | |||
<summary class="right-content tw-py-2{{if .Err_SMTP}} text red{{end}}"> | |||
{{ctx.Locale.Tr "install.email_title"}} | |||
</summary> | |||
<div class="inline field"> | |||
<label for="smtp_addr">{{ctx.Locale.Tr "install.smtp_addr"}}</label> | |||
<input id="smtp_addr" name="smtp_addr" value="{{.smtp_addr}}"> | |||
</div> | |||
<div class="inline field"> | |||
<label for="smtp_port">{{ctx.Locale.Tr "install.smtp_port"}}</label> | |||
<input id="smtp_port" name="smtp_port" value="{{.smtp_port}}"> | |||
</div> | |||
<div class="inline field {{if .Err_SMTPFrom}}error{{end}}"> | |||
<label for="smtp_from">{{ctx.Locale.Tr "install.smtp_from"}}</label> | |||
<input id="smtp_from" name="smtp_from" value="{{.smtp_from}}"> | |||
<span class="help">{{ctx.Locale.TrString "install.smtp_from_helper"}}{{/* it contains lt/gt chars*/}}</span> | |||
</div> | |||
<div class="inline field {{if .Err_SMTPUser}}error{{end}}"> | |||
<label for="smtp_user">{{ctx.Locale.Tr "install.mailer_user"}}</label> | |||
<input id="smtp_user" name="smtp_user" value="{{.smtp_user}}"> | |||
</div> | |||
<div class="inline field"> | |||
<label for="smtp_passwd">{{ctx.Locale.Tr "install.mailer_password"}}</label> | |||
<input id="smtp_passwd" name="smtp_passwd" type="password" value="{{.smtp_passwd}}"> | |||
</div> | |||
<div class="inline field"> | |||
<div class="ui checkbox"> | |||
<label>{{ctx.Locale.Tr "install.register_confirm"}}</label> | |||
<input name="register_confirm" type="checkbox" {{if .register_confirm}}checked{{end}}> | |||
<div> | |||
<!-- Email --> | |||
<details class="optional field"> | |||
<summary class="right-content tw-py-2{{if .Err_SMTP}} text red{{end}}"> | |||
{{ctx.Locale.Tr "install.email_title"}} | |||
</summary> | |||
<div class="inline field"> | |||
<label for="smtp_addr">{{ctx.Locale.Tr "install.smtp_addr"}}</label> | |||
<input id="smtp_addr" name="smtp_addr" value="{{.smtp_addr}}"> | |||
</div> | |||
</div> | |||
<div class="inline field"> | |||
<div class="ui checkbox"> | |||
<label>{{ctx.Locale.Tr "install.mail_notify"}}</label> | |||
<input name="mail_notify" type="checkbox" {{if .mail_notify}}checked{{end}}> | |||
<div class="inline field"> | |||
<label for="smtp_port">{{ctx.Locale.Tr "install.smtp_port"}}</label> | |||
<input id="smtp_port" name="smtp_port" value="{{.smtp_port}}"> | |||
</div> | |||
</div> | |||
</details> | |||
<div class="inline field {{if .Err_SMTPFrom}}error{{end}}"> | |||
<label for="smtp_from">{{ctx.Locale.Tr "install.smtp_from"}}</label> | |||
<input id="smtp_from" name="smtp_from" value="{{.smtp_from}}"> | |||
<span class="help">{{ctx.Locale.TrString "install.smtp_from_helper"}}{{/* it contains lt/gt chars*/}}</span> | |||
</div> | |||
<div class="inline field {{if .Err_SMTPUser}}error{{end}}"> | |||
<label for="smtp_user">{{ctx.Locale.Tr "install.mailer_user"}}</label> | |||
<input id="smtp_user" name="smtp_user" value="{{.smtp_user}}"> | |||
</div> | |||
<div class="inline field"> | |||
<label for="smtp_passwd">{{ctx.Locale.Tr "install.mailer_password"}}</label> | |||
<input id="smtp_passwd" name="smtp_passwd" type="password" value="{{.smtp_passwd}}"> | |||
</div> | |||
<div class="inline field"> | |||
<div class="ui checkbox"> | |||
<label>{{ctx.Locale.Tr "install.register_confirm"}}</label> | |||
<input name="register_confirm" type="checkbox" {{if .register_confirm}}checked{{end}}> | |||
</div> | |||
</div> | |||
<div class="inline field"> | |||
<div class="ui checkbox"> | |||
<label>{{ctx.Locale.Tr "install.mail_notify"}}</label> | |||
<input name="mail_notify" type="checkbox" {{if .mail_notify}}checked{{end}}> | |||
</div> | |||
</div> | |||
</details> | |||
<!-- Server and other services --> | |||
<details class="optional field"> | |||
<summary class="right-content tw-py-2{{if .Err_Services}} text red{{end}}"> | |||
{{ctx.Locale.Tr "install.server_service_title"}} | |||
</summary> | |||
<div class="inline field"> | |||
<div class="ui checkbox" id="offline-mode"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.offline_mode_popup"}}">{{ctx.Locale.Tr "install.offline_mode"}}</label> | |||
<input name="offline_mode" type="checkbox" {{if .offline_mode}}checked{{end}}> | |||
<!-- Server and other services --> | |||
<details class="optional field"> | |||
<summary class="right-content tw-py-2{{if .Err_Services}} text red{{end}}"> | |||
{{ctx.Locale.Tr "install.server_service_title"}} | |||
</summary> | |||
<div class="inline field"> | |||
<div class="ui checkbox" id="offline-mode"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.offline_mode_popup"}}">{{ctx.Locale.Tr "install.offline_mode"}}</label> | |||
<input name="offline_mode" type="checkbox" {{if .offline_mode}}checked{{end}}> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="inline field"> | |||
<div class="ui checkbox" id="disable-gravatar"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.disable_gravatar_popup"}}">{{ctx.Locale.Tr "install.disable_gravatar"}}</label> | |||
<input name="disable_gravatar" type="checkbox" {{if .disable_gravatar}}checked{{end}}> | |||
<div class="inline field"> | |||
<div class="ui checkbox" id="disable-gravatar"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.disable_gravatar_popup"}}">{{ctx.Locale.Tr "install.disable_gravatar"}}</label> | |||
<input name="disable_gravatar" type="checkbox" {{if .disable_gravatar}}checked{{end}}> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="inline field"> | |||
<div class="ui checkbox" id="federated-avatar-lookup"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.federated_avatar_lookup_popup"}}">{{ctx.Locale.Tr "install.federated_avatar_lookup"}}</label> | |||
<input name="enable_federated_avatar" type="checkbox" {{if .enable_federated_avatar}}checked{{end}}> | |||
<div class="inline field"> | |||
<div class="ui checkbox" id="federated-avatar-lookup"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.federated_avatar_lookup_popup"}}">{{ctx.Locale.Tr "install.federated_avatar_lookup"}}</label> | |||
<input name="enable_federated_avatar" type="checkbox" {{if .enable_federated_avatar}}checked{{end}}> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="inline field"> | |||
<div class="ui checkbox" id="enable-openid-signin"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.openid_signin_popup"}}">{{ctx.Locale.Tr "install.openid_signin"}}</label> | |||
<input name="enable_open_id_sign_in" type="checkbox" {{if .enable_open_id_sign_in}}checked{{end}}> | |||
<div class="inline field"> | |||
<div class="ui checkbox" id="enable-openid-signin"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.openid_signin_popup"}}">{{ctx.Locale.Tr "install.openid_signin"}}</label> | |||
<input name="enable_open_id_sign_in" type="checkbox" {{if .enable_open_id_sign_in}}checked{{end}}> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="inline field"> | |||
<div class="ui checkbox" id="disable-registration"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.disable_registration_popup"}}">{{ctx.Locale.Tr "install.disable_registration"}}</label> | |||
<input name="disable_registration" type="checkbox" {{if .disable_registration}}checked{{end}}> | |||
<div class="inline field"> | |||
<div class="ui checkbox" id="disable-registration"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.disable_registration_popup"}}">{{ctx.Locale.Tr "install.disable_registration"}}</label> | |||
<input name="disable_registration" type="checkbox" {{if .disable_registration}}checked{{end}}> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="inline field"> | |||
<div class="ui checkbox" id="allow-only-external-registration"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.allow_only_external_registration_popup"}}">{{ctx.Locale.Tr "install.allow_only_external_registration_popup"}}</label> | |||
<input name="allow_only_external_registration" type="checkbox" {{if .allow_only_external_registration}}checked{{end}}> | |||
<div class="inline field"> | |||
<div class="ui checkbox" id="allow-only-external-registration"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.allow_only_external_registration_popup"}}">{{ctx.Locale.Tr "install.allow_only_external_registration_popup"}}</label> | |||
<input name="allow_only_external_registration" type="checkbox" {{if .allow_only_external_registration}}checked{{end}}> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="inline field"> | |||
<div class="ui checkbox" id="enable-openid-signup"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.openid_signup_popup"}}">{{ctx.Locale.Tr "install.openid_signup"}}</label> | |||
<input name="enable_open_id_sign_up" type="checkbox" {{if .enable_open_id_sign_up}}checked{{end}}> | |||
<div class="inline field"> | |||
<div class="ui checkbox" id="enable-openid-signup"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.openid_signup_popup"}}">{{ctx.Locale.Tr "install.openid_signup"}}</label> | |||
<input name="enable_open_id_sign_up" type="checkbox" {{if .enable_open_id_sign_up}}checked{{end}}> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="inline field"> | |||
<div class="ui checkbox" id="enable-captcha"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.enable_captcha_popup"}}">{{ctx.Locale.Tr "install.enable_captcha"}}</label> | |||
<input name="enable_captcha" type="checkbox" {{if .enable_captcha}}checked{{end}}> | |||
<div class="inline field"> | |||
<div class="ui checkbox" id="enable-captcha"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.enable_captcha_popup"}}">{{ctx.Locale.Tr "install.enable_captcha"}}</label> | |||
<input name="enable_captcha" type="checkbox" {{if .enable_captcha}}checked{{end}}> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="inline field"> | |||
<div class="ui checkbox"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.require_sign_in_view_popup"}}">{{ctx.Locale.Tr "install.require_sign_in_view"}}</label> | |||
<input name="require_sign_in_view" type="checkbox" {{if .require_sign_in_view}}checked{{end}}> | |||
<div class="inline field"> | |||
<div class="ui checkbox"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.require_sign_in_view_popup"}}">{{ctx.Locale.Tr "install.require_sign_in_view"}}</label> | |||
<input name="require_sign_in_view" type="checkbox" {{if .require_sign_in_view}}checked{{end}}> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="inline field"> | |||
<div class="ui checkbox"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_keep_email_private_popup"}}">{{ctx.Locale.Tr "install.default_keep_email_private"}}</label> | |||
<input name="default_keep_email_private" type="checkbox" {{if .default_keep_email_private}}checked{{end}}> | |||
<div class="inline field"> | |||
<div class="ui checkbox"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_keep_email_private_popup"}}">{{ctx.Locale.Tr "install.default_keep_email_private"}}</label> | |||
<input name="default_keep_email_private" type="checkbox" {{if .default_keep_email_private}}checked{{end}}> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="inline field"> | |||
<div class="ui checkbox"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_allow_create_organization_popup"}}">{{ctx.Locale.Tr "install.default_allow_create_organization"}}</label> | |||
<input name="default_allow_create_organization" type="checkbox" {{if .default_allow_create_organization}}checked{{end}}> | |||
<div class="inline field"> | |||
<div class="ui checkbox"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_allow_create_organization_popup"}}">{{ctx.Locale.Tr "install.default_allow_create_organization"}}</label> | |||
<input name="default_allow_create_organization" type="checkbox" {{if .default_allow_create_organization}}checked{{end}}> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="inline field"> | |||
<div class="ui checkbox"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_enable_timetracking_popup"}}">{{ctx.Locale.Tr "install.default_enable_timetracking"}}</label> | |||
<input name="default_enable_timetracking" type="checkbox" {{if .default_enable_timetracking}}checked{{end}}> | |||
<div class="inline field"> | |||
<div class="ui checkbox"> | |||
<label data-tooltip-content="{{ctx.Locale.Tr "install.default_enable_timetracking_popup"}}">{{ctx.Locale.Tr "install.default_enable_timetracking"}}</label> | |||
<input name="default_enable_timetracking" type="checkbox" {{if .default_enable_timetracking}}checked{{end}}> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="inline field"> | |||
<label for="no_reply_address">{{ctx.Locale.Tr "install.no_reply_address"}}</label> | |||
<input id="_no_reply_address" name="no_reply_address" value="{{.no_reply_address}}"> | |||
<span class="help">{{ctx.Locale.Tr "install.no_reply_address_helper"}}</span> | |||
</div> | |||
<div class="inline field"> | |||
<label for="password_algorithm">{{ctx.Locale.Tr "install.password_algorithm"}}</label> | |||
<div class="ui selection dropdown"> | |||
<input id="password_algorithm" type="hidden" name="password_algorithm" value="{{.password_algorithm}}"> | |||
<div class="text">{{.password_algorithm}}</div> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
<div class="menu"> | |||
{{range .PasswordHashAlgorithms}} | |||
<div class="item" data-value="{{.}}">{{.}}</div> | |||
{{end}} | |||
<div class="inline field"> | |||
<label for="no_reply_address">{{ctx.Locale.Tr "install.no_reply_address"}}</label> | |||
<input id="_no_reply_address" name="no_reply_address" value="{{.no_reply_address}}"> | |||
<span class="help">{{ctx.Locale.Tr "install.no_reply_address_helper"}}</span> | |||
</div> | |||
<div class="inline field"> | |||
<label for="password_algorithm">{{ctx.Locale.Tr "install.password_algorithm"}}</label> | |||
<div class="ui selection dropdown"> | |||
<input id="password_algorithm" type="hidden" name="password_algorithm" value="{{.password_algorithm}}"> | |||
<div class="text">{{.password_algorithm}}</div> | |||
{{svg "octicon-triangle-down" 14 "dropdown icon"}} | |||
<div class="menu"> | |||
{{range .PasswordHashAlgorithms}} | |||
<div class="item" data-value="{{.}}">{{.}}</div> | |||
{{end}} | |||
</div> | |||
</div> | |||
<span class="help">{{ctx.Locale.Tr "install.password_algorithm_helper"}}</span> | |||
</div> | |||
<span class="help">{{ctx.Locale.Tr "install.password_algorithm_helper"}}</span> | |||
</div> | |||
</details> | |||
</details> | |||
<!-- Admin --> | |||
<details class="optional field"> | |||
<summary class="right-content tw-py-2{{if .Err_Admin}} text red{{end}}"> | |||
{{ctx.Locale.Tr "install.admin_title"}} | |||
</summary> | |||
<p class="center">{{ctx.Locale.Tr "install.admin_setting_desc"}}</p> | |||
<div class="inline field {{if .Err_AdminName}}error{{end}}"> | |||
<label for="admin_name">{{ctx.Locale.Tr "install.admin_name"}}</label> | |||
<input id="admin_name" name="admin_name" value="{{.admin_name}}"> | |||
</div> | |||
<div class="inline field {{if .Err_AdminEmail}}error{{end}}"> | |||
<label for="admin_email">{{ctx.Locale.Tr "install.admin_email"}}</label> | |||
<input id="admin_email" name="admin_email" type="email" value="{{.admin_email}}"> | |||
</div> | |||
<div class="inline field {{if .Err_AdminPasswd}}error{{end}}"> | |||
<label for="admin_passwd">{{ctx.Locale.Tr "install.admin_password"}}</label> | |||
<input id="admin_passwd" name="admin_passwd" type="password" autocomplete="new-password" value="{{.admin_passwd}}"> | |||
</div> | |||
<div class="inline field {{if .Err_AdminPasswd}}error{{end}}"> | |||
<label for="admin_confirm_passwd">{{ctx.Locale.Tr "install.confirm_password"}}</label> | |||
<input id="admin_confirm_passwd" name="admin_confirm_passwd" autocomplete="new-password" type="password" value="{{.admin_confirm_passwd}}"> | |||
</div> | |||
</details> | |||
<!-- Admin --> | |||
<details class="optional field"> | |||
<summary class="right-content tw-py-2{{if .Err_Admin}} text red{{end}}"> | |||
{{ctx.Locale.Tr "install.admin_title"}} | |||
</summary> | |||
<p class="center">{{ctx.Locale.Tr "install.admin_setting_desc"}}</p> | |||
<div class="inline field {{if .Err_AdminName}}error{{end}}"> | |||
<label for="admin_name">{{ctx.Locale.Tr "install.admin_name"}}</label> | |||
<input id="admin_name" name="admin_name" value="{{.admin_name}}"> | |||
</div> | |||
<div class="inline field {{if .Err_AdminEmail}}error{{end}}"> | |||
<label for="admin_email">{{ctx.Locale.Tr "install.admin_email"}}</label> | |||
<input id="admin_email" name="admin_email" type="email" value="{{.admin_email}}"> | |||
</div> | |||
<div class="inline field {{if .Err_AdminPasswd}}error{{end}}"> | |||
<label for="admin_passwd">{{ctx.Locale.Tr "install.admin_password"}}</label> | |||
<input id="admin_passwd" name="admin_passwd" type="password" autocomplete="new-password" value="{{.admin_passwd}}"> | |||
</div> | |||
<div class="inline field {{if .Err_AdminPasswd}}error{{end}}"> | |||
<label for="admin_confirm_passwd">{{ctx.Locale.Tr "install.confirm_password"}}</label> | |||
<input id="admin_confirm_passwd" name="admin_confirm_passwd" autocomplete="new-password" type="password" value="{{.admin_confirm_passwd}}"> | |||
</div> | |||
</details> | |||
</div> | |||
<div class="divider"></div> | |||
{{if .EnvConfigKeys}} | |||
<!-- Environment Config --> | |||
@@ -333,12 +336,11 @@ | |||
</div> | |||
{{end}} | |||
<div class="divider"></div> | |||
<div class="inline field"> | |||
<div class="right-content"> | |||
These configuration options will be written into: {{.CustomConfFile}} | |||
</div> | |||
<div class="right-content tw-mt-2"> | |||
<div class="tw-mt-4 tw-mb-2 tw-text-center"> | |||
<button class="ui primary button">{{ctx.Locale.Tr "install.install_btn_confirm"}}</button> | |||
</div> | |||
</div> |
@@ -69,7 +69,7 @@ | |||
<div class="js-branch-tag-selector {{if .ContainerClasses}}{{.ContainerClasses}}{{end}}"> | |||
{{/* show dummy elements before Vue componment is mounted, this code must match the code in BranchTagSelector.vue */}} | |||
<div class="ui dropdown custom branch-selector-dropdown"> | |||
<div class="ui dropdown custom branch-selector-dropdown ellipsis-items-nowrap"> | |||
<div class="ui button branch-dropdown-button"> | |||
<span class="flex-text-block gt-ellipsis"> | |||
{{if .release}} |
@@ -128,107 +128,109 @@ | |||
{{if .IsGenerated}}<div class="fork-flag">{{ctx.Locale.Tr "repo.generated_from"}} <a href="{{(.TemplateRepo ctx).Link}}">{{(.TemplateRepo ctx).FullName}}</a></div>{{end}} | |||
</div> | |||
{{end}} | |||
<overflow-menu class="ui container secondary pointing tabular top attached borderless menu tw-pt-0 tw-my-0"> | |||
{{if not (or .Repository.IsBeingCreated .Repository.IsBroken)}} | |||
<div class="overflow-menu-items"> | |||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeCode}} | |||
<a class="{{if .PageIsViewCode}}active {{end}}item" href="{{.RepoLink}}{{if and (ne .BranchName .Repository.DefaultBranch) (not $.PageIsWiki)}}/src/{{.BranchNameSubURL}}{{end}}"> | |||
{{svg "octicon-code"}} {{ctx.Locale.Tr "repo.code"}} | |||
</a> | |||
{{end}} | |||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeIssues}} | |||
<a class="{{if .PageIsIssueList}}active {{end}}item" href="{{.RepoLink}}/issues"> | |||
{{svg "octicon-issue-opened"}} {{ctx.Locale.Tr "repo.issues"}} | |||
{{if .Repository.NumOpenIssues}} | |||
<span class="ui small label">{{CountFmt .Repository.NumOpenIssues}}</span> | |||
{{end}} | |||
<div class="ui container"> | |||
<overflow-menu class="ui secondary pointing menu"> | |||
{{if not (or .Repository.IsBeingCreated .Repository.IsBroken)}} | |||
<div class="overflow-menu-items"> | |||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeCode}} | |||
<a class="{{if .PageIsViewCode}}active {{end}}item" href="{{.RepoLink}}{{if and (ne .BranchName .Repository.DefaultBranch) (not $.PageIsWiki)}}/src/{{.BranchNameSubURL}}{{end}}"> | |||
{{svg "octicon-code"}} {{ctx.Locale.Tr "repo.code"}} | |||
</a> | |||
{{end}} | |||
{{end}} | |||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeExternalTracker}} | |||
<a class="{{if .PageIsIssueList}}active {{end}}item" href="{{.RepoExternalIssuesLink}}" target="_blank" rel="noopener noreferrer"> | |||
{{svg "octicon-link-external"}} {{ctx.Locale.Tr "repo.issues"}} | |||
</a> | |||
{{end}} | |||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeIssues}} | |||
<a class="{{if .PageIsIssueList}}active {{end}}item" href="{{.RepoLink}}/issues"> | |||
{{svg "octicon-issue-opened"}} {{ctx.Locale.Tr "repo.issues"}} | |||
{{if .Repository.NumOpenIssues}} | |||
<span class="ui small label">{{CountFmt .Repository.NumOpenIssues}}</span> | |||
{{end}} | |||
</a> | |||
{{end}} | |||
{{if and .Repository.CanEnablePulls (.Permission.CanRead ctx.Consts.RepoUnitTypePullRequests)}} | |||
<a class="{{if .PageIsPullList}}active {{end}}item" href="{{.RepoLink}}/pulls"> | |||
{{svg "octicon-git-pull-request"}} {{ctx.Locale.Tr "repo.pulls"}} | |||
{{if .Repository.NumOpenPulls}} | |||
<span class="ui small label">{{CountFmt .Repository.NumOpenPulls}}</span> | |||
{{end}} | |||
</a> | |||
{{end}} | |||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeExternalTracker}} | |||
<a class="{{if .PageIsIssueList}}active {{end}}item" href="{{.RepoExternalIssuesLink}}" target="_blank" rel="noopener noreferrer"> | |||
{{svg "octicon-link-external"}} {{ctx.Locale.Tr "repo.issues"}} | |||
</a> | |||
{{end}} | |||
{{if and .EnableActions (not .UnitActionsGlobalDisabled) (.Permission.CanRead ctx.Consts.RepoUnitTypeActions)}} | |||
<a class="{{if .PageIsActions}}active {{end}}item" href="{{.RepoLink}}/actions"> | |||
{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}} | |||
{{if .Repository.NumOpenActionRuns}} | |||
<span class="ui small label">{{CountFmt .Repository.NumOpenActionRuns}}</span> | |||
{{end}} | |||
</a> | |||
{{end}} | |||
{{if and .Repository.CanEnablePulls (.Permission.CanRead ctx.Consts.RepoUnitTypePullRequests)}} | |||
<a class="{{if .PageIsPullList}}active {{end}}item" href="{{.RepoLink}}/pulls"> | |||
{{svg "octicon-git-pull-request"}} {{ctx.Locale.Tr "repo.pulls"}} | |||
{{if .Repository.NumOpenPulls}} | |||
<span class="ui small label">{{CountFmt .Repository.NumOpenPulls}}</span> | |||
{{end}} | |||
</a> | |||
{{end}} | |||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypePackages}} | |||
<a href="{{.RepoLink}}/packages" class="{{if .IsPackagesPage}}active {{end}}item"> | |||
{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}} | |||
</a> | |||
{{end}} | |||
{{if and .EnableActions (not .UnitActionsGlobalDisabled) (.Permission.CanRead ctx.Consts.RepoUnitTypeActions)}} | |||
<a class="{{if .PageIsActions}}active {{end}}item" href="{{.RepoLink}}/actions"> | |||
{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}} | |||
{{if .Repository.NumOpenActionRuns}} | |||
<span class="ui small label">{{CountFmt .Repository.NumOpenActionRuns}}</span> | |||
{{end}} | |||
</a> | |||
{{end}} | |||
{{$projectsUnit := .Repository.MustGetUnit $.Context ctx.Consts.RepoUnitTypeProjects}} | |||
{{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead ctx.Consts.RepoUnitTypeProjects) ($projectsUnit.ProjectsConfig.IsProjectsAllowed "repo")}} | |||
<a href="{{.RepoLink}}/projects" class="{{if .IsProjectsPage}}active {{end}}item"> | |||
{{svg "octicon-project"}} {{ctx.Locale.Tr "repo.project_board"}} | |||
{{if .Repository.NumOpenProjects}} | |||
<span class="ui small label">{{CountFmt .Repository.NumOpenProjects}}</span> | |||
{{end}} | |||
</a> | |||
{{end}} | |||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypePackages}} | |||
<a href="{{.RepoLink}}/packages" class="{{if .IsPackagesPage}}active {{end}}item"> | |||
{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}} | |||
</a> | |||
{{end}} | |||
{{if and (.Permission.CanRead ctx.Consts.RepoUnitTypeReleases) (not .IsEmptyRepo)}} | |||
<a class="{{if or .PageIsReleaseList .PageIsTagList}}active {{end}}item" href="{{.RepoLink}}/releases"> | |||
{{svg "octicon-tag"}} {{ctx.Locale.Tr "repo.releases"}} | |||
{{if .NumReleases}} | |||
<span class="ui small label">{{CountFmt .NumReleases}}</span> | |||
{{end}} | |||
</a> | |||
{{end}} | |||
{{$projectsUnit := .Repository.MustGetUnit $.Context ctx.Consts.RepoUnitTypeProjects}} | |||
{{if and (not .UnitProjectsGlobalDisabled) (.Permission.CanRead ctx.Consts.RepoUnitTypeProjects) ($projectsUnit.ProjectsConfig.IsProjectsAllowed "repo")}} | |||
<a href="{{.RepoLink}}/projects" class="{{if .IsProjectsPage}}active {{end}}item"> | |||
{{svg "octicon-project"}} {{ctx.Locale.Tr "repo.project_board"}} | |||
{{if .Repository.NumOpenProjects}} | |||
<span class="ui small label">{{CountFmt .Repository.NumOpenProjects}}</span> | |||
{{end}} | |||
</a> | |||
{{end}} | |||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeWiki}} | |||
<a class="{{if .PageIsWiki}}active {{end}}item" href="{{.RepoLink}}/wiki"> | |||
{{svg "octicon-book"}} {{ctx.Locale.Tr "repo.wiki"}} | |||
{{if and (.Permission.CanRead ctx.Consts.RepoUnitTypeReleases) (not .IsEmptyRepo)}} | |||
<a class="{{if or .PageIsReleaseList .PageIsTagList}}active {{end}}item" href="{{.RepoLink}}/releases"> | |||
{{svg "octicon-tag"}} {{ctx.Locale.Tr "repo.releases"}} | |||
{{if .NumReleases}} | |||
<span class="ui small label">{{CountFmt .NumReleases}}</span> | |||
{{end}} | |||
</a> | |||
{{end}} | |||
{{end}} | |||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeExternalWiki}} | |||
<a class="item" href="{{(.Repository.MustGetUnit $.Context ctx.Consts.RepoUnitTypeExternalWiki).ExternalWikiConfig.ExternalWikiURL}}" target="_blank" rel="noopener noreferrer"> | |||
{{svg "octicon-link-external"}} {{ctx.Locale.Tr "repo.wiki"}} | |||
</a> | |||
{{end}} | |||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeWiki}} | |||
<a class="{{if .PageIsWiki}}active {{end}}item" href="{{.RepoLink}}/wiki"> | |||
{{svg "octicon-book"}} {{ctx.Locale.Tr "repo.wiki"}} | |||
</a> | |||
{{end}} | |||
{{if and (.Permission.CanReadAny ctx.Consts.RepoUnitTypePullRequests ctx.Consts.RepoUnitTypeIssues ctx.Consts.RepoUnitTypeReleases) (not .IsEmptyRepo)}} | |||
<a class="{{if .PageIsActivity}}active {{end}}item" href="{{.RepoLink}}/activity"> | |||
{{svg "octicon-pulse"}} {{ctx.Locale.Tr "repo.activity"}} | |||
</a> | |||
{{end}} | |||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypeExternalWiki}} | |||
<a class="item" href="{{(.Repository.MustGetUnit $.Context ctx.Consts.RepoUnitTypeExternalWiki).ExternalWikiConfig.ExternalWikiURL}}" target="_blank" rel="noopener noreferrer"> | |||
{{svg "octicon-link-external"}} {{ctx.Locale.Tr "repo.wiki"}} | |||
</a> | |||
{{end}} | |||
{{template "custom/extra_tabs" .}} | |||
{{if and (.Permission.CanReadAny ctx.Consts.RepoUnitTypePullRequests ctx.Consts.RepoUnitTypeIssues ctx.Consts.RepoUnitTypeReleases) (not .IsEmptyRepo)}} | |||
<a class="{{if .PageIsActivity}}active {{end}}item" href="{{.RepoLink}}/activity"> | |||
{{svg "octicon-pulse"}} {{ctx.Locale.Tr "repo.activity"}} | |||
</a> | |||
{{end}} | |||
{{if .Permission.IsAdmin}} | |||
<span class="item-flex-space"></span> | |||
{{template "custom/extra_tabs" .}} | |||
{{if .Permission.IsAdmin}} | |||
<span class="item-flex-space"></span> | |||
<a class="{{if .PageIsRepoSettings}}active {{end}} item" href="{{.RepoLink}}/settings"> | |||
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}} | |||
</a> | |||
{{end}} | |||
</div> | |||
{{else if .Permission.IsAdmin}} | |||
<div class="overflow-menu-items"> | |||
<a class="{{if .PageIsRepoSettings}}active {{end}} item" href="{{.RepoLink}}/settings"> | |||
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}} | |||
</a> | |||
{{end}} | |||
</div> | |||
{{else if .Permission.IsAdmin}} | |||
<div class="overflow-menu-items"> | |||
<a class="{{if .PageIsRepoSettings}}active {{end}} item" href="{{.RepoLink}}/settings"> | |||
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}} | |||
</a> | |||
</div> | |||
{{end}} | |||
</overflow-menu> | |||
</div> | |||
{{end}} | |||
</overflow-menu> | |||
</div> | |||
<div class="ui tabs divider"></div> | |||
</div> |
@@ -4,7 +4,7 @@ | |||
<form method="post" action="{{$.RepoLink}}/issues/{{.Issue.Index}}/ref" id="update_issueref_form"> | |||
{{$.CsrfTokenHtml}} | |||
</form> | |||
<div class="ui dropdown select-branch branch-selector-dropdown {{if not .HasIssuesOrPullsWritePermission}}disabled{{end}}" | |||
<div class="ui dropdown select-branch branch-selector-dropdown ellipsis-items-nowrap {{if not .HasIssuesOrPullsWritePermission}}disabled{{end}}" | |||
data-no-results="{{ctx.Locale.Tr "no_results_found"}}" | |||
{{if not .Issue}}data-for-new-issue="true"{{end}} | |||
> |
@@ -7,7 +7,7 @@ | |||
{{.CsrfTokenHtml}} | |||
<div class="field"> | |||
<label><strong>{{ctx.Locale.Tr "repository"}}</strong></label> | |||
<div class="ui search selection dropdown issue_reference_repository_search"> | |||
<div class="ui search selection dropdown issue_reference_repository_search ellipsis-items-nowrap"> | |||
<div class="default text gt-ellipsis">{{.Repository.FullName}}</div> | |||
<div class="menu"></div> | |||
</div> |
@@ -4,29 +4,36 @@ | |||
</div> | |||
{{end}} | |||
<div class="issue-title-header"> | |||
<div class="issue-title" id="issue-title-wrapper"> | |||
{{$canEditIssueTitle := and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}} | |||
<div class="issue-title" id="issue-title-display"> | |||
<h1 class="gt-word-break"> | |||
<span id="issue-title">{{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}} <span class="index">#{{.Issue.Index}}</span> | |||
</span> | |||
<div id="edit-title-input" class="ui input tw-flex-1 tw-hidden"> | |||
<input value="{{.Issue.Title}}" maxlength="255" autocomplete="off"> | |||
</div> | |||
{{RenderIssueTitle $.Context .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}} | |||
<span class="index">#{{.Issue.Index}}</span> | |||
</h1> | |||
<div class="issue-title-buttons"> | |||
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}} | |||
<button id="edit-title" class="ui small basic button edit-button not-in-edit{{if .Issue.IsPull}} tw-mr-0{{end}}">{{ctx.Locale.Tr "repo.issues.edit"}}</button> | |||
{{if $canEditIssueTitle}} | |||
<button id="issue-title-edit-show" class="ui small basic button">{{ctx.Locale.Tr "repo.issues.edit"}}</button> | |||
{{end}} | |||
{{if not .Issue.IsPull}} | |||
<a role="button" class="ui small primary button new-issue-button tw-mr-0" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a> | |||
<a role="button" class="ui small primary button" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{ctx.Locale.Tr "repo.issues.new"}}</a> | |||
{{end}} | |||
</div> | |||
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}} | |||
<div class="edit-buttons"> | |||
<button id="cancel-edit-title" class="ui small basic button in-edit tw-hidden">{{ctx.Locale.Tr "repo.issues.cancel"}}</button> | |||
<button id="save-edit-title" class="ui small primary button in-edit tw-hidden tw-mr-0" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title" {{if .Issue.IsPull}}data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch"{{end}}>{{ctx.Locale.Tr "repo.issues.save"}}</button> | |||
</div> | |||
{{end}} | |||
</div> | |||
{{if $canEditIssueTitle}} | |||
<div class="ui form issue-title tw-hidden" id="issue-title-editor"> | |||
<div class="ui input tw-flex-1"> | |||
<input value="{{.Issue.Title}}" data-old-title="{{.Issue.Title}}" maxlength="255" autocomplete="off"> | |||
</div> | |||
<div class="issue-title-buttons"> | |||
<button class="ui small basic cancel button">{{ctx.Locale.Tr "repo.issues.cancel"}}</button> | |||
<button class="ui small primary button" | |||
data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/title" | |||
{{if .Issue.IsPull}}data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch"{{end}}> | |||
{{ctx.Locale.Tr "repo.issues.save"}} | |||
</button> | |||
</div> | |||
</div> | |||
{{end}} | |||
<div class="issue-title-meta"> | |||
{{if .HasMerged}} | |||
<div class="ui purple label issue-state-label">{{svg "octicon-git-merge" 16 "tw-mr-1"}} {{if eq .Issue.PullRequest.Status 3}}{{ctx.Locale.Tr "repo.pulls.manually_merged"}}{{else}}{{ctx.Locale.Tr "repo.pulls.merged"}}{{end}}</div> | |||
@@ -63,14 +70,14 @@ | |||
{{end}} | |||
{{else}} | |||
{{if .Issue.OriginalAuthor}} | |||
<span id="pull-desc" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}</span> | |||
<span id="pull-desc-display" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}}</span> | |||
{{else}} | |||
<span id="pull-desc" class="pull-desc"> | |||
<span id="pull-desc-display" class="pull-desc"> | |||
<a {{if gt .Issue.Poster.ID 0}}href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.GetDisplayName}}</a> | |||
{{ctx.Locale.Tr "repo.pulls.title_desc" .NumCommits $headHref $baseHref}} | |||
</span> | |||
{{end}} | |||
<span id="pull-desc-edit" class="tw-hidden flex-text-block"> | |||
<span id="pull-desc-editor" class="tw-hidden flex-text-block"> | |||
<div class="ui floating filter dropdown"> | |||
<div class="ui basic small button tw-mr-0"> | |||
<span class="text">{{ctx.Locale.Tr "repo.pulls.compare_compare"}}: {{$.HeadTarget}}</span> |
@@ -345,7 +345,7 @@ | |||
<div class="inline field"> | |||
{{$unitInternalWiki := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeWiki}} | |||
<label>{{ctx.Locale.Tr "repo.settings.default_wiki_everyone_access"}}</label> | |||
<select name="default_wiki_everyone_access" class="ui dropdown"> | |||
<select name="default_wiki_everyone_access" class="ui selection dropdown"> | |||
{{/* everyone access mode is different from others, none means it is unset and won't be applied */}} | |||
<option value="none" {{Iif (eq $unitInternalWiki.EveryoneAccessMode 0) "selected"}}>{{ctx.Locale.Tr "settings.permission_not_set"}}</option> | |||
<option value="read" {{Iif (eq $unitInternalWiki.EveryoneAccessMode 1) "selected"}}>{{ctx.Locale.Tr "settings.permission_read"}}</option> |
@@ -195,14 +195,17 @@ func TestAPIEditUser(t *testing.T) { | |||
token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin) | |||
urlStr := fmt.Sprintf("/api/v1/admin/users/%s", "user2") | |||
fullNameToChange := "Full Name User 2" | |||
req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{ | |||
// required | |||
"login_name": "user2", | |||
"source_id": "0", | |||
// to change | |||
"full_name": "Full Name User 2", | |||
"full_name": fullNameToChange, | |||
}).AddTokenAuth(token) | |||
MakeRequest(t, req, http.StatusOK) | |||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"}) | |||
assert.Equal(t, fullNameToChange, user2.FullName) | |||
empty := "" | |||
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{ | |||
@@ -216,7 +219,7 @@ func TestAPIEditUser(t *testing.T) { | |||
json.Unmarshal(resp.Body.Bytes(), &errMap) | |||
assert.EqualValues(t, "e-mail invalid [email: ]", errMap["message"].(string)) | |||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"}) | |||
user2 = unittest.AssertExistsAndLoadBean(t, &user_model.User{LoginName: "user2"}) | |||
assert.False(t, user2.IsRestricted) | |||
bTrue := true | |||
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditUserOption{ |
@@ -194,6 +194,10 @@ func TestAPIEditIssue(t *testing.T) { | |||
issueAfter := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10}) | |||
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 | |||
assert.Equal(t, int64(500), issueAfter.PosterID) | |||
assert.NoError(t, issueAfter.LoadAttributes(db.DefaultContext)) |
@@ -223,23 +223,33 @@ func TestAPIEditPull(t *testing.T) { | |||
session := loginUser(t, owner10.Name) | |||
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{ | |||
Head: "develop", | |||
Base: "master", | |||
Title: "create a success pr", | |||
Title: title, | |||
}).AddTokenAuth(token) | |||
pull := new(api.PullRequest) | |||
apiPull := new(api.PullRequest) | |||
resp := MakeRequest(t, req, http.StatusCreated) | |||
DecodeJSON(t, resp, pull) | |||
assert.EqualValues(t, "master", pull.Base.Name) | |||
DecodeJSON(t, resp, apiPull) | |||
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", | |||
Title: "edit a this pr", | |||
Title: newTitle, | |||
Body: &newBody, | |||
}).AddTokenAuth(token) | |||
resp = MakeRequest(t, req, http.StatusCreated) | |||
DecodeJSON(t, resp, pull) | |||
assert.EqualValues(t, "feature/1", pull.Base.Name) | |||
DecodeJSON(t, resp, apiPull) | |||
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{ | |||
Base: "not-exist", |
@@ -13,6 +13,7 @@ import ( | |||
"code.gitea.io/gitea/models/db" | |||
access_model "code.gitea.io/gitea/models/perm/access" | |||
repo_model "code.gitea.io/gitea/models/repo" | |||
unit_model "code.gitea.io/gitea/models/unit" | |||
"code.gitea.io/gitea/models/unittest" | |||
user_model "code.gitea.io/gitea/models/user" | |||
"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) { | |||
defer tests.PrepareTestEnv(t)() | |||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) |
@@ -144,7 +144,7 @@ func testNewIssue(t *testing.T, session *TestSession, user, repo, title, content | |||
resp = session.MakeRequest(t, req, http.StatusOK) | |||
htmlDoc = NewHTMLParser(t, resp.Body) | |||
val := htmlDoc.doc.Find("#issue-title").Text() | |||
val := htmlDoc.doc.Find("#issue-title-display").Text() | |||
assert.Contains(t, val, title) | |||
val = htmlDoc.doc.Find(".comment .render-content p").First().Text() | |||
assert.Equal(t, content, val) |
@@ -125,7 +125,7 @@ func TestPullCreate_TitleEscape(t *testing.T) { | |||
req := NewRequest(t, "GET", url) | |||
resp = session.MakeRequest(t, req, http.StatusOK) | |||
htmlDoc := NewHTMLParser(t, resp.Body) | |||
editTestTitleURL, exists := htmlDoc.doc.Find("#save-edit-title").First().Attr("data-update-url") | |||
editTestTitleURL, exists := htmlDoc.doc.Find(".issue-title-buttons button[data-update-url]").First().Attr("data-update-url") | |||
assert.True(t, exists, "The template has changed") | |||
req = NewRequestWithValues(t, "POST", editTestTitleURL, map[string]string{ |
@@ -342,8 +342,6 @@ a.label, | |||
.ui.dropdown .menu > .item { | |||
color: var(--color-text); | |||
overflow: hidden; | |||
text-overflow: ellipsis; | |||
} | |||
.ui.dropdown .menu > .item:hover { | |||
@@ -374,7 +372,6 @@ a.label, | |||
.ui.selection.dropdown .menu > .item { | |||
border-color: var(--color-secondary); | |||
white-space: nowrap; | |||
} | |||
.ui.selection.visible.dropdown > .text:not(.default) { | |||
@@ -1342,7 +1339,11 @@ table th[data-sortt-desc] .svg { | |||
align-items: center; | |||
gap: .25rem; | |||
vertical-align: middle; | |||
min-width: 0; | |||
min-width: 0; /* make ellipsis work */ | |||
} | |||
.ui.ui.dropdown.selection { | |||
min-width: 14em; /* match the default min width */ | |||
} | |||
.ui.dropdown .ui.label .svg { | |||
@@ -1369,3 +1370,16 @@ table th[data-sortt-desc] .svg { | |||
gap: .5rem; | |||
min-width: 0; | |||
} | |||
.ui.dropdown.ellipsis-items-nowrap > .text { | |||
overflow: hidden; | |||
white-space: nowrap; | |||
text-overflow: ellipsis; | |||
} | |||
.ellipsis-items-nowrap > .item, | |||
.ui.dropdown.ellipsis-items-nowrap .menu > .item { | |||
white-space: nowrap !important; | |||
overflow: hidden !important; | |||
text-overflow: ellipsis !important; | |||
} |
@@ -448,6 +448,10 @@ textarea:focus, | |||
} | |||
} | |||
.ui.form .field > .selection.dropdown { | |||
min-width: 14em; /* matches the default min width */ | |||
} | |||
.new.webhook form .help { | |||
margin-left: 25px; | |||
} |
@@ -13,8 +13,7 @@ | |||
.page-content.install .ui.form .field > .help, | |||
.page-content.install .ui.form .field > .ui.checkbox:first-child, | |||
.page-content.install .ui.form .field > .right-content { | |||
margin-left: 30%; | |||
padding-left: 5px; | |||
margin-left: calc(30% + 5px); | |||
width: auto; | |||
} | |||
@@ -24,10 +23,11 @@ | |||
} | |||
.page-content.install form.ui.form details.optional.field[open] { | |||
border-bottom: 1px dashed var(--color-secondary); | |||
padding-bottom: 10px; | |||
} | |||
.page-content.install form.ui.form details.optional.field[open]:not(:last-child) { | |||
border-bottom: 1px dashed var(--color-secondary); | |||
} | |||
.page-content.install form.ui.form details.optional.field[open] summary { | |||
margin-bottom: 10px; | |||
} |
@@ -41,7 +41,7 @@ input[type="radio"] { | |||
.ui.checkbox label, | |||
.ui.radio.checkbox label { | |||
margin-left: 1.85714em; | |||
margin-left: 20px; | |||
} | |||
.ui.checkbox + label { |
@@ -2,26 +2,20 @@ | |||
unused rules here after refactoring, please remove them. */ | |||
.ui.container { | |||
display: block; | |||
max-width: 100%; | |||
} | |||
.ui.fluid.container { | |||
width: 100%; | |||
} | |||
.ui[class*="center aligned"].container { | |||
text-align: center; | |||
} | |||
/* overwrite width of containers inside the main page content div (div with class "page-content") */ | |||
.page-content .ui.ui.ui.container:not(.fluid) { | |||
width: 1280px; | |||
max-width: calc(100% - calc(2 * var(--page-margin-x))); | |||
margin-left: auto; | |||
margin-right: auto; | |||
} | |||
.ui.fluid.container { | |||
width: 100%; | |||
} | |||
.ui.container.fluid.padded { | |||
padding: 0 var(--page-margin-x); | |||
} | |||
.ui[class*="center aligned"].container { | |||
text-align: center; | |||
} |
@@ -575,6 +575,22 @@ td .commit-summary { | |||
display: inline-block; | |||
} | |||
@media (max-width: 767.98px) { | |||
.comment.form .issue-content-left .avatar { | |||
display: none; | |||
} | |||
.comment.form .issue-content-left .content { | |||
margin-left: 0 !important; | |||
} | |||
.comment.form .issue-content-left .content::before, | |||
.comment.form .issue-content-left .content::after, | |||
.comment.form .content .form::before, | |||
.comment.form .content .form::after { | |||
display: none; | |||
} | |||
} | |||
/* issue title & meta & edit */ | |||
.issue-title-header { | |||
width: 100%; | |||
padding-bottom: 4px; | |||
@@ -586,46 +602,25 @@ td .commit-summary { | |||
align-items: center; | |||
} | |||
.repository.view.issue .issue-title-buttons, | |||
.repository.view.issue .edit-buttons { | |||
.repository.view.issue .issue-title-buttons { | |||
display: flex; | |||
gap: 0.5em; | |||
} | |||
@media (max-width: 767.98px) { | |||
.repository.view.issue .issue-title { | |||
flex-direction: column; | |||
} | |||
.repository.view.issue .issue-title-buttons, | |||
.repository.view.issue .edit-buttons { | |||
width: 100%; | |||
justify-content: space-between; | |||
} | |||
.repository.view.issue .edit-buttons { | |||
margin-top: .5rem; | |||
} | |||
.comment.form .issue-content-left .avatar { | |||
display: none; | |||
} | |||
.comment.form .issue-content-left .content { | |||
margin-left: 0 !important; | |||
} | |||
.comment.form .issue-content-left .content::before, | |||
.comment.form .issue-content-left .content::after, | |||
.comment.form .content .form::before, | |||
.comment.form .content .form::after { | |||
display: none; | |||
} | |||
.repository.view.issue .issue-title-buttons > .ui.button { | |||
margin: 0; | |||
height: 35px; | |||
} | |||
.repository.view.issue .issue-title { | |||
display: flex; | |||
align-items: center; | |||
gap: 0.5em; | |||
margin-bottom: 8px; | |||
min-height: 40px; /* avoid layout shift on edit */ | |||
} | |||
.repository.view.issue .issue-title h1 { | |||
display: flex; | |||
align-items: center; | |||
flex: 1; | |||
width: 100%; | |||
font-weight: var(--font-weight-normal); | |||
@@ -633,14 +628,24 @@ td .commit-summary { | |||
line-height: 40px; | |||
margin: 0; | |||
padding-right: 0.25rem; | |||
min-height: 41px; /* avoid layout shift on edit */ | |||
} | |||
.repository.view.issue .issue-title h1 .ui.input { | |||
font-size: 0.5em; | |||
@media (max-width: 767.98px) { | |||
.repository.view.issue .issue-title { | |||
flex-direction: column; | |||
} | |||
.repository.view.issue .issue-title-buttons { | |||
width: 100%; | |||
justify-content: space-between; | |||
} | |||
} | |||
.repository.view.issue .issue-title .ui.input { | |||
width: 100%; | |||
height: 35px; | |||
} | |||
.repository.view.issue .issue-title h1 .ui.input input { | |||
.repository.view.issue .issue-title .ui.input input { | |||
font-size: 1.5em; | |||
padding: 2px .5rem; | |||
} | |||
@@ -653,10 +658,6 @@ td .commit-summary { | |||
margin-right: 10px; | |||
} | |||
.issue-title .edit-zone { | |||
margin-top: 10px; | |||
} | |||
.issue-state-label { | |||
display: flex !important; | |||
align-items: center !important; | |||
@@ -2859,6 +2860,10 @@ tbody.commit-list { | |||
margin-top: 4px; | |||
} | |||
.ui.dropdown.branch-selector-dropdown .scrolling.menu { | |||
max-width: min(400px, 90vw); | |||
} | |||
.branch-selector-dropdown .branch-dropdown-button { | |||
margin: 0; | |||
max-width: 340px; | |||
@@ -2908,6 +2913,8 @@ tbody.commit-list { | |||
} | |||
.branch-selector-dropdown .menu .item .rss-icon { | |||
position: absolute; | |||
right: 4px; | |||
visibility: hidden; /* only show RSS icon on hover */ | |||
} | |||
@@ -6,13 +6,20 @@ | |||
// This file must be imported before any lazy-loading is being attempted. | |||
__webpack_public_path__ = `${window.config?.assetUrlPrefix ?? '/assets'}/`; | |||
export function showGlobalErrorMessage(msg) { | |||
const pageContent = document.querySelector('.page-content'); | |||
if (!pageContent) return; | |||
function shouldIgnoreError(err) { | |||
const ignorePatterns = [ | |||
'/assets/js/monaco.', // https://github.com/go-gitea/gitea/issues/30861 , https://github.com/microsoft/monaco-editor/issues/4496 | |||
]; | |||
for (const pattern of ignorePatterns) { | |||
if (err.stack?.includes(pattern)) return true; | |||
} | |||
return false; | |||
} | |||
// compact the message to a data attribute to avoid too many duplicated messages | |||
const msgCompact = msg.replace(/\W/g, '').trim(); | |||
let msgDiv = pageContent.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`); | |||
export function showGlobalErrorMessage(msg) { | |||
const msgContainer = document.querySelector('.page-content') ?? document.body; | |||
const msgCompact = msg.replace(/\W/g, '').trim(); // compact the message to a data attribute to avoid too many duplicated messages | |||
let msgDiv = msgContainer.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`); | |||
if (!msgDiv) { | |||
const el = document.createElement('div'); | |||
el.innerHTML = `<div class="ui container negative message center aligned js-global-error tw-mt-[15px] tw-whitespace-pre-line"></div>`; | |||
@@ -23,7 +30,7 @@ export function showGlobalErrorMessage(msg) { | |||
msgDiv.setAttribute(`data-global-error-msg-compact`, msgCompact); | |||
msgDiv.setAttribute(`data-global-error-msg-count`, msgCount.toString()); | |||
msgDiv.textContent = msg + (msgCount > 1 ? ` (${msgCount})` : ''); | |||
pageContent.prepend(msgDiv); | |||
msgContainer.prepend(msgDiv); | |||
} | |||
/** | |||
@@ -52,10 +59,12 @@ function processWindowErrorEvent({error, reason, message, type, filename, lineno | |||
if (runModeIsProd) return; | |||
} | |||
// If the error stack trace does not include the base URL of our script assets, it likely came | |||
// from a browser extension or inline script. Do not show such errors in production. | |||
if (err instanceof Error && !err.stack?.includes(assetBaseUrl) && runModeIsProd) { | |||
return; | |||
if (err instanceof Error) { | |||
// If the error stack trace does not include the base URL of our script assets, it likely came | |||
// from a browser extension or inline script. Do not show such errors in production. | |||
if (!err.stack?.includes(assetBaseUrl) && runModeIsProd) return; | |||
// Ignore some known errors that are unable to fix | |||
if (shouldIgnoreError(err)) return; | |||
} | |||
let msg = err?.message ?? message; |
@@ -246,7 +246,7 @@ export function initRepoBranchTagSelector(selector) { | |||
export default sfc; // activate IDE's Vue plugin | |||
</script> | |||
<template> | |||
<div class="ui dropdown custom branch-selector-dropdown"> | |||
<div class="ui dropdown custom branch-selector-dropdown ellipsis-items-nowrap"> | |||
<div class="ui button branch-dropdown-button" @click="menuVisible = !menuVisible" @keyup.enter="menuVisible = !menuVisible"> | |||
<span class="flex-text-block gt-ellipsis"> | |||
<template v-if="release">{{ textReleaseCompare }}</template> | |||
@@ -280,7 +280,7 @@ export default sfc; // activate IDE's Vue plugin | |||
<div class="ui label" v-if="item.name===repoDefaultBranch && mode === 'branches'"> | |||
{{ textDefaultBranchLabel }} | |||
</div> | |||
<a v-show="enableFeed && mode === 'branches'" role="button" class="rss-icon tw-float-right" :href="rssURLPrefix + item.url" target="_blank" @click.stop> | |||
<a v-show="enableFeed && mode === 'branches'" role="button" class="rss-icon" :href="rssURLPrefix + item.url" target="_blank" @click.stop> | |||
<!-- creating a lot of Vue component is pretty slow, so we use a static SVG here --> | |||
<svg width="14" height="14" class="svg octicon-rss"><use href="#svg-symbol-octicon-rss"/></svg> | |||
</a> |
@@ -67,7 +67,7 @@ export default { | |||
const weekValues = Object.values(this.data); | |||
const start = weekValues[0].week; | |||
const end = firstStartDateAfterDate(new Date()); | |||
const startDays = startDaysBetween(new Date(start), new Date(end)); | |||
const startDays = startDaysBetween(start, end); | |||
this.data = fillEmptyStartDaysWithZeroes(startDays, this.data); | |||
this.errorText = ''; | |||
} else { |
@@ -114,7 +114,7 @@ export default { | |||
const weekValues = Object.values(total.weeks); | |||
this.xAxisStart = weekValues[0].week; | |||
this.xAxisEnd = firstStartDateAfterDate(new Date()); | |||
const startDays = startDaysBetween(new Date(this.xAxisStart), new Date(this.xAxisEnd)); | |||
const startDays = startDaysBetween(this.xAxisStart, this.xAxisEnd); | |||
total.weeks = fillEmptyStartDaysWithZeroes(startDays, total.weeks); | |||
this.xAxisMin = this.xAxisStart; | |||
this.xAxisMax = this.xAxisEnd; |
@@ -62,7 +62,7 @@ export default { | |||
const data = await response.json(); | |||
const start = Object.values(data)[0].week; | |||
const end = firstStartDateAfterDate(new Date()); | |||
const startDays = startDaysBetween(new Date(start), new Date(end)); | |||
const startDays = startDaysBetween(start, end); | |||
this.data = fillEmptyStartDaysWithZeroes(startDays, data).slice(-52); | |||
this.errorText = ''; | |||
} else { |
@@ -47,10 +47,18 @@ export function initFootLanguageMenu() { | |||
export function initGlobalEnterQuickSubmit() { | |||
document.addEventListener('keydown', (e) => { | |||
const isQuickSubmitEnter = ((e.ctrlKey && !e.altKey) || e.metaKey) && (e.key === 'Enter'); | |||
if (isQuickSubmitEnter && e.target.matches('textarea')) { | |||
e.preventDefault(); | |||
handleGlobalEnterQuickSubmit(e.target); | |||
if (e.key !== 'Enter') return; | |||
const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey); | |||
if (hasCtrlOrMeta && e.target.matches('textarea')) { | |||
if (handleGlobalEnterQuickSubmit(e.target)) { | |||
e.preventDefault(); | |||
} | |||
} else if (e.target.matches('input') && !e.target.closest('form')) { | |||
// input in a normal form could handle Enter key by default, so we only handle the input outside a form | |||
// eslint-disable-next-line unicorn/no-lonely-if | |||
if (handleGlobalEnterQuickSubmit(e.target)) { | |||
e.preventDefault(); | |||
} | |||
} | |||
}); | |||
} |
@@ -3,16 +3,17 @@ export function handleGlobalEnterQuickSubmit(target) { | |||
if (form) { | |||
if (!form.checkValidity()) { | |||
form.reportValidity(); | |||
return; | |||
} else { | |||
// here use the event to trigger the submit event (instead of calling `submit()` method directly) | |||
// otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog | |||
form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true})); | |||
} | |||
// here use the event to trigger the submit event (instead of calling `submit()` method directly) | |||
// otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog | |||
form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true})); | |||
return; | |||
return true; | |||
} | |||
form = target.closest('.ui.form'); | |||
if (form) { | |||
form.querySelector('.ui.primary.button')?.click(); | |||
return true; | |||
} | |||
return false; | |||
} |
@@ -7,6 +7,7 @@ import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkd | |||
import {toAbsoluteUrl} from '../utils.js'; | |||
import {initDropzone} from './common-global.js'; | |||
import {POST, GET} from '../modules/fetch.js'; | |||
import {showErrorToast} from '../modules/toast.js'; | |||
const {appSubUrl} = window.config; | |||
@@ -123,8 +124,8 @@ export function initRepoIssueSidebarList() { | |||
return; | |||
} | |||
filteredResponse.results.push({ | |||
name: `#${issue.number} ${htmlEscape(issue.title) | |||
}<div class="text small gt-word-break">${htmlEscape(issue.repository.full_name)}</div>`, | |||
name: `<div class="gt-ellipsis">#${issue.number} ${htmlEscape(issue.title)}</div> | |||
<div class="text small gt-word-break">${htmlEscape(issue.repository.full_name)}</div>`, | |||
value: issue.id, | |||
}); | |||
}); | |||
@@ -298,23 +299,23 @@ export function initRepoPullRequestMergeInstruction() { | |||
export function initRepoPullRequestAllowMaintainerEdit() { | |||
const wrapper = document.getElementById('allow-edits-from-maintainers'); | |||
if (!wrapper) return; | |||
wrapper.querySelector('input[type="checkbox"]')?.addEventListener('change', async (e) => { | |||
const checked = e.target.checked; | |||
const checkbox = wrapper.querySelector('input[type="checkbox"]'); | |||
checkbox.addEventListener('input', async () => { | |||
const url = `${wrapper.getAttribute('data-url')}/set_allow_maintainer_edit`; | |||
wrapper.classList.add('is-loading'); | |||
e.target.disabled = true; | |||
try { | |||
const response = await POST(url, {data: {allow_maintainer_edit: checked}}); | |||
if (!response.ok) { | |||
const resp = await POST(url, {data: new URLSearchParams({allow_maintainer_edit: checkbox.checked})}); | |||
if (!resp.ok) { | |||
throw new Error('Failed to update maintainer edit permission'); | |||
} | |||
const data = await resp.json(); | |||
checkbox.checked = data.allow_maintainer_edit; | |||
} catch (error) { | |||
checkbox.checked = !checkbox.checked; | |||
console.error(error); | |||
showTemporaryTooltip(wrapper, wrapper.getAttribute('data-prompt-error')); | |||
} finally { | |||
wrapper.classList.remove('is-loading'); | |||
e.target.disabled = false; | |||
} | |||
}); | |||
} | |||
@@ -602,85 +603,69 @@ export function initRepoIssueWipToggle() { | |||
}); | |||
} | |||
async function pullrequest_targetbranch_change(update_url) { | |||
const targetBranch = $('#pull-target-branch').data('branch'); | |||
const $branchTarget = $('#branch_target'); | |||
if (targetBranch === $branchTarget.text()) { | |||
window.location.reload(); | |||
return false; | |||
} | |||
try { | |||
await POST(update_url, {data: new URLSearchParams({target_branch: targetBranch})}); | |||
} catch (error) { | |||
console.error(error); | |||
} finally { | |||
window.location.reload(); | |||
} | |||
} | |||
export function initRepoIssueTitleEdit() { | |||
// Edit issue title | |||
const $issueTitle = $('#issue-title'); | |||
const $editInput = $('#edit-title-input input'); | |||
const editTitleToggle = function () { | |||
toggleElem($issueTitle); | |||
toggleElem('.not-in-edit'); | |||
toggleElem('#edit-title-input'); | |||
toggleElem('#pull-desc'); | |||
toggleElem('#pull-desc-edit'); | |||
toggleElem('.in-edit'); | |||
toggleElem('.new-issue-button'); | |||
document.getElementById('issue-title-wrapper')?.classList.toggle('edit-active'); | |||
$editInput[0].focus(); | |||
$editInput[0].select(); | |||
return false; | |||
}; | |||
$('#edit-title').on('click', editTitleToggle); | |||
$('#cancel-edit-title').on('click', editTitleToggle); | |||
$('#save-edit-title').on('click', editTitleToggle).on('click', async function () { | |||
const pullrequest_target_update_url = this.getAttribute('data-target-update-url'); | |||
if (!$editInput.val().length || $editInput.val() === $issueTitle.text()) { | |||
$editInput.val($issueTitle.text()); | |||
await pullrequest_targetbranch_change(pullrequest_target_update_url); | |||
} else { | |||
try { | |||
const params = new URLSearchParams(); | |||
params.append('title', $editInput.val()); | |||
const response = await POST(this.getAttribute('data-update-url'), {data: params}); | |||
const data = await response.json(); | |||
$editInput.val(data.title); | |||
$issueTitle.text(data.title); | |||
if (pullrequest_target_update_url) { | |||
await pullrequest_targetbranch_change(pullrequest_target_update_url); // it will reload the window | |||
} else { | |||
window.location.reload(); | |||
const issueTitleDisplay = document.querySelector('#issue-title-display'); | |||
const issueTitleEditor = document.querySelector('#issue-title-editor'); | |||
if (!issueTitleEditor) return; | |||
const issueTitleInput = issueTitleEditor.querySelector('input'); | |||
const oldTitle = issueTitleInput.getAttribute('data-old-title'); | |||
issueTitleDisplay.querySelector('#issue-title-edit-show').addEventListener('click', () => { | |||
hideElem(issueTitleDisplay); | |||
hideElem('#pull-desc-display'); | |||
showElem(issueTitleEditor); | |||
showElem('#pull-desc-editor'); | |||
if (!issueTitleInput.value.trim()) { | |||
issueTitleInput.value = oldTitle; | |||
} | |||
issueTitleInput.focus(); | |||
}); | |||
issueTitleEditor.querySelector('.ui.cancel.button').addEventListener('click', () => { | |||
hideElem(issueTitleEditor); | |||
hideElem('#pull-desc-editor'); | |||
showElem(issueTitleDisplay); | |||
showElem('#pull-desc-display'); | |||
}); | |||
const editSaveButton = issueTitleEditor.querySelector('.ui.primary.button'); | |||
editSaveButton.addEventListener('click', async () => { | |||
const prTargetUpdateUrl = editSaveButton.getAttribute('data-target-update-url'); | |||
const newTitle = issueTitleInput.value.trim(); | |||
try { | |||
if (newTitle && newTitle !== oldTitle) { | |||
const resp = await POST(editSaveButton.getAttribute('data-update-url'), {data: new URLSearchParams({title: newTitle})}); | |||
if (!resp.ok) { | |||
throw new Error(`Failed to update issue title: ${resp.statusText}`); | |||
} | |||
} | |||
if (prTargetUpdateUrl) { | |||
const newTargetBranch = document.querySelector('#pull-target-branch').getAttribute('data-branch'); | |||
const oldTargetBranch = document.querySelector('#branch_target').textContent; | |||
if (newTargetBranch !== oldTargetBranch) { | |||
const resp = await POST(prTargetUpdateUrl, {data: new URLSearchParams({target_branch: newTargetBranch})}); | |||
if (!resp.ok) { | |||
throw new Error(`Failed to update PR target branch: ${resp.statusText}`); | |||
} | |||
} | |||
} catch (error) { | |||
console.error(error); | |||
} | |||
window.location.reload(); | |||
} catch (error) { | |||
console.error(error); | |||
showErrorToast(error.message); | |||
} | |||
return false; | |||
}); | |||
} | |||
export function initRepoIssueBranchSelect() { | |||
const changeBranchSelect = function () { | |||
const $selectionTextField = $('#pull-target-branch'); | |||
const baseName = $selectionTextField.data('basename'); | |||
const branchNameNew = $(this).data('branch'); | |||
const branchNameOld = $selectionTextField.data('branch'); | |||
// Replace branch name to keep translation from HTML template | |||
$selectionTextField.html($selectionTextField.html().replace( | |||
`${baseName}:${branchNameOld}`, | |||
`${baseName}:${branchNameNew}`, | |||
)); | |||
$selectionTextField.data('branch', branchNameNew); // update branch name in setting | |||
}; | |||
$('#branch-select > .item').on('click', changeBranchSelect); | |||
document.querySelector('#branch-select')?.addEventListener('click', (e) => { | |||
const el = e.target.closest('.item[data-branch]'); | |||
if (!el) return; | |||
const pullTargetBranch = document.querySelector('#pull-target-branch'); | |||
const baseName = pullTargetBranch.getAttribute('data-basename'); | |||
const branchNameNew = el.getAttribute('data-branch'); | |||
const branchNameOld = pullTargetBranch.getAttribute('data-branch'); | |||
pullTargetBranch.textContent = pullTargetBranch.textContent.replace(`${baseName}:${branchNameOld}`, `${baseName}:${branchNameNew}`); | |||
pullTargetBranch.setAttribute('data-branch', branchNameNew); | |||
}); | |||
} | |||
export function initSingleCommentEditor($commentForm) { |
@@ -57,6 +57,7 @@ export function initRepoCommentForm() { | |||
function initBranchSelector() { | |||
const elSelectBranch = document.querySelector('.ui.dropdown.select-branch'); | |||
if (!elSelectBranch) return; | |||
const isForNewIssue = elSelectBranch.getAttribute('data-for-new-issue') === 'true'; | |||
const $selectBranch = $(elSelectBranch); |
@@ -3,11 +3,12 @@ import {queryElemChildren} from '../../utils/dom.js'; | |||
export function initFomanticDimmer() { | |||
// stand-in for removed dimmer module | |||
$.fn.dimmer = function (arg0, $el) { | |||
$.fn.dimmer = function (arg0, arg1) { | |||
if (arg0 === 'add content') { | |||
const $el = arg1; | |||
const existingDimmer = document.querySelector('body > .ui.dimmer'); | |||
if (existingDimmer) { | |||
queryElemChildren(existingDimmer, '*', (el) => el.remove()); | |||
queryElemChildren(existingDimmer, '*', (el) => el.classList.add('hidden')); | |||
this._dimmer = existingDimmer; | |||
} else { | |||
this._dimmer = document.createElement('div'); | |||
@@ -21,8 +22,10 @@ export function initFomanticDimmer() { | |||
this._dimmer.classList.add('active'); | |||
document.body.classList.add('tw-overflow-hidden'); | |||
} else if (arg0 === 'hide') { | |||
const cb = arg1; | |||
this._dimmer.classList.remove('active'); | |||
document.body.classList.remove('tw-overflow-hidden'); | |||
cb(); | |||
} | |||
return this; | |||
}; |
@@ -1,25 +1,30 @@ | |||
import dayjs from 'dayjs'; | |||
import utc from 'dayjs/plugin/utc.js'; | |||
import {getCurrentLocale} from '../utils.js'; | |||
// Returns an array of millisecond-timestamps of start-of-week days (Sundays) | |||
dayjs.extend(utc); | |||
/** | |||
* Returns an array of millisecond-timestamps of start-of-week days (Sundays) | |||
* | |||
* @param startConfig The start date. Can take any type that `Date` accepts. | |||
* @param endConfig The end date. Can take any type that `Date` accepts. | |||
*/ | |||
export function startDaysBetween(startDate, endDate) { | |||
const start = dayjs.utc(startDate); | |||
const end = dayjs.utc(endDate); | |||
let current = start; | |||
// Ensure the start date is a Sunday | |||
while (startDate.getDay() !== 0) { | |||
startDate.setDate(startDate.getDate() + 1); | |||
while (current.day() !== 0) { | |||
current = current.add(1, 'day'); | |||
} | |||
const start = dayjs(startDate); | |||
const end = dayjs(endDate); | |||
const startDays = []; | |||
let current = start; | |||
while (current.isBefore(end)) { | |||
startDays.push(current.valueOf()); | |||
// we are adding 7 * 24 hours instead of 1 week because we don't want | |||
// date library to use local time zone to calculate 1 week from now. | |||
// local time zone is problematic because of daylight saving time (dst) | |||
// used on some countries | |||
current = current.add(7 * 24, 'hour'); | |||
current = current.add(1, 'week'); | |||
} | |||
return startDays; | |||
@@ -29,10 +34,10 @@ export function firstStartDateAfterDate(inputDate) { | |||
if (!(inputDate instanceof Date)) { | |||
throw new Error('Invalid date'); | |||
} | |||
const dayOfWeek = inputDate.getDay(); | |||
const dayOfWeek = inputDate.getUTCDay(); | |||
const daysUntilSunday = 7 - dayOfWeek; | |||
const resultDate = new Date(inputDate.getTime()); | |||
resultDate.setDate(resultDate.getDate() + daysUntilSunday); | |||
resultDate.setUTCDate(resultDate.getUTCDate() + daysUntilSunday); | |||
return resultDate.valueOf(); | |||
} | |||