Merge branch 'main' into feat-32257-add-comments-unchanged-lines-and-show

This commit is contained in:
Rajesh Jonnalagadda 2024-12-04 19:32:43 +05:30 committed by GitHub
commit c1e4d5766a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
74 changed files with 571 additions and 621 deletions

View File

@ -1,80 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//nolint:forbidigo
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/unittest"
)
// To generate derivative fixtures, execute the following from Gitea's repository base dir:
// go run -tags 'sqlite sqlite_unlock_notify' contrib/fixtures/fixture_generation.go [fixture...]
var (
generators = []struct {
gen func(ctx context.Context) (string, error)
name string
}{
{
models.GetYamlFixturesAccess, "access",
},
}
fixturesDir string
)
func main() {
pathToGiteaRoot := "."
fixturesDir = filepath.Join(pathToGiteaRoot, "models", "fixtures")
if err := unittest.CreateTestEngine(unittest.FixturesOptions{
Dir: fixturesDir,
}); err != nil {
fmt.Printf("CreateTestEngine: %+v", err)
os.Exit(1)
}
if err := unittest.PrepareTestDatabase(); err != nil {
fmt.Printf("PrepareTestDatabase: %+v\n", err)
os.Exit(1)
}
ctx := context.Background()
if len(os.Args) == 0 {
for _, r := range os.Args {
if err := generate(ctx, r); err != nil {
fmt.Printf("generate '%s': %+v\n", r, err)
os.Exit(1)
}
}
} else {
for _, g := range generators {
if err := generate(ctx, g.name); err != nil {
fmt.Printf("generate '%s': %+v\n", g.name, err)
os.Exit(1)
}
}
}
}
func generate(ctx context.Context, name string) error {
for _, g := range generators {
if g.name == name {
data, err := g.gen(ctx)
if err != nil {
return err
}
path := filepath.Join(fixturesDir, name+".yml")
if err := os.WriteFile(path, []byte(data), 0o644); err != nil {
return fmt.Errorf("%s: %+v", path, err)
}
fmt.Printf("%s created.\n", path)
return nil
}
}
return fmt.Errorf("generator not found")
}

View File

@ -1,50 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package models
import (
"context"
"fmt"
"strings"
"code.gitea.io/gitea/models/db"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
)
// GetYamlFixturesAccess returns a string containing the contents
// for the access table, as recalculated using repo.RecalculateAccesses()
func GetYamlFixturesAccess(ctx context.Context) (string, error) {
repos := make([]*repo_model.Repository, 0, 50)
if err := db.GetEngine(ctx).Find(&repos); err != nil {
return "", err
}
for _, repo := range repos {
repo.MustOwner(ctx)
if err := access_model.RecalculateAccesses(ctx, repo); err != nil {
return "", err
}
}
var b strings.Builder
accesses := make([]*access_model.Access, 0, 200)
if err := db.GetEngine(ctx).OrderBy("user_id, repo_id").Find(&accesses); err != nil {
return "", err
}
for i, a := range accesses {
fmt.Fprintf(&b, "-\n")
fmt.Fprintf(&b, " id: %d\n", i+1)
fmt.Fprintf(&b, " user_id: %d\n", a.UserID)
fmt.Fprintf(&b, " repo_id: %d\n", a.RepoID)
fmt.Fprintf(&b, " mode: %d\n", a.Mode)
if i < len(accesses)-1 {
fmt.Fprintf(&b, "\n")
}
}
return b.String(), nil
}

View File

@ -1,37 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package models
import (
"context"
"os"
"path/filepath"
"testing"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
)
func TestFixtureGeneration(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
test := func(ctx context.Context, gen func(ctx context.Context) (string, error), name string) {
expected, err := gen(ctx)
if !assert.NoError(t, err) {
return
}
p := filepath.Join(unittest.FixturesDir(), name+".yml")
bytes, err := os.ReadFile(p)
if !assert.NoError(t, err) {
return
}
data := string(util.NormalizeEOL(bytes))
assert.EqualValues(t, expected, data, "Differences detected for %s", p)
}
test(db.DefaultContext, GetYamlFixturesAccess, "access")
}

View File

@ -617,7 +617,7 @@ func (repo *Repository) CanEnableEditor() bool {
// DescriptionHTML does special handles to description and return HTML string. // DescriptionHTML does special handles to description and return HTML string.
func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML { func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML {
desc, err := markup.RenderDescriptionHTML(markup.NewRenderContext(ctx), repo.Description) desc, err := markup.PostProcessDescriptionHTML(markup.NewRenderContext(ctx), repo.Description)
if err != nil { if err != nil {
log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err) log.Error("Failed to render description for %s (ID: %d): %v", repo.Name, repo.ID, err)
return template.HTML(markup.SanitizeDescription(repo.Description)) return template.HTML(markup.SanitizeDescription(repo.Description))

View File

@ -14,9 +14,16 @@ import (
"github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/object"
) )
// GetRefCommitID returns the last commit ID string of given reference (branch or tag). // GetRefCommitID returns the last commit ID string of given reference.
func (repo *Repository) GetRefCommitID(name string) (string, error) { func (repo *Repository) GetRefCommitID(name string) (string, error) {
ref, err := repo.gogitRepo.Reference(plumbing.ReferenceName(name), true) if plumbing.IsHash(name) {
return name, nil
}
refName := plumbing.ReferenceName(name)
if err := refName.Validate(); err != nil {
return "", err
}
ref, err := repo.gogitRepo.Reference(refName, true)
if err != nil { if err != nil {
if err == plumbing.ErrReferenceNotFound { if err == plumbing.ErrReferenceNotFound {
return "", ErrNotExist{ return "", ErrNotExist{

View File

@ -101,3 +101,28 @@ func TestRepository_CommitsBetweenIDs(t *testing.T) {
assert.Len(t, commits, c.ExpectedCommits, "case %d", i) assert.Len(t, commits, c.ExpectedCommits, "case %d", i)
} }
} }
func TestGetRefCommitID(t *testing.T) {
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
bareRepo1, err := openRepositoryWithDefaultContext(bareRepo1Path)
assert.NoError(t, err)
defer bareRepo1.Close()
// these test case are specific to the repo1_bare test repo
testCases := []struct {
Ref string
ExpectedCommitID string
}{
{RefNameFromBranch("master").String(), "ce064814f4a0d337b333e646ece456cd39fab612"},
{RefNameFromBranch("branch1").String(), "2839944139e0de9737a044f78b0e4b40d989a9e3"},
{RefNameFromTag("test").String(), "3ad28a9149a2864384548f3d17ed7f38014c9e8a"},
{"ce064814f4a0d337b333e646ece456cd39fab612", "ce064814f4a0d337b333e646ece456cd39fab612"},
}
for _, testCase := range testCases {
commitID, err := bareRepo1.GetRefCommitID(testCase.Ref)
if assert.NoError(t, err) {
assert.Equal(t, testCase.ExpectedCommitID, commitID)
}
}
}

View File

@ -159,9 +159,9 @@ func PostProcessDefault(ctx *RenderContext, input io.Reader, output io.Writer) e
return postProcess(ctx, procs, input, output) return postProcess(ctx, procs, input, output)
} }
// RenderCommitMessage will use the same logic as PostProcess, but will disable // PostProcessCommitMessage will use the same logic as PostProcess, but will disable
// the shortLinkProcessor. // the shortLinkProcessor.
func RenderCommitMessage(ctx *RenderContext, content string) (string, error) { func PostProcessCommitMessage(ctx *RenderContext, content string) (string, error) {
procs := []processor{ procs := []processor{
fullIssuePatternProcessor, fullIssuePatternProcessor,
comparePatternProcessor, comparePatternProcessor,
@ -183,11 +183,11 @@ var emojiProcessors = []processor{
emojiProcessor, emojiProcessor,
} }
// RenderCommitMessageSubject will use the same logic as PostProcess and // PostProcessCommitMessageSubject will use the same logic as PostProcess and
// RenderCommitMessage, but will disable the shortLinkProcessor and // PostProcessCommitMessage, but will disable the shortLinkProcessor and
// emailAddressProcessor, will add a defaultLinkProcessor if defaultLink is set, // emailAddressProcessor, will add a defaultLinkProcessor if defaultLink is set,
// which changes every text node into a link to the passed default link. // which changes every text node into a link to the passed default link.
func RenderCommitMessageSubject(ctx *RenderContext, defaultLink, content string) (string, error) { func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content string) (string, error) {
procs := []processor{ procs := []processor{
fullIssuePatternProcessor, fullIssuePatternProcessor,
comparePatternProcessor, comparePatternProcessor,
@ -211,15 +211,33 @@ func RenderCommitMessageSubject(ctx *RenderContext, defaultLink, content string)
return postProcessString(ctx, procs, content) return postProcessString(ctx, procs, content)
} }
// RenderIssueTitle to process title on individual issue/pull page // PostProcessIssueTitle to process title on individual issue/pull page
func RenderIssueTitle(ctx *RenderContext, title string) (string, error) { func PostProcessIssueTitle(ctx *RenderContext, title string) (string, error) {
// do not render other issue/commit links in an issue's title - which in most cases is already a link.
return postProcessString(ctx, []processor{ return postProcessString(ctx, []processor{
issueIndexPatternProcessor,
commitCrossReferencePatternProcessor,
hashCurrentPatternProcessor,
emojiShortCodeProcessor, emojiShortCodeProcessor,
emojiProcessor, emojiProcessor,
}, title) }, title)
} }
// PostProcessDescriptionHTML will use similar logic as PostProcess, but will
// use a single special linkProcessor.
func PostProcessDescriptionHTML(ctx *RenderContext, content string) (string, error) {
return postProcessString(ctx, []processor{
descriptionLinkProcessor,
emojiShortCodeProcessor,
emojiProcessor,
}, content)
}
// PostProcessEmoji for when we want to just process emoji and shortcodes
// in various places it isn't already run through the normal markdown processor
func PostProcessEmoji(ctx *RenderContext, content string) (string, error) {
return postProcessString(ctx, emojiProcessors, content)
}
func postProcessString(ctx *RenderContext, procs []processor, content string) (string, error) { func postProcessString(ctx *RenderContext, procs []processor, content string) (string, error) {
var buf strings.Builder var buf strings.Builder
if err := postProcess(ctx, procs, strings.NewReader(content), &buf); err != nil { if err := postProcess(ctx, procs, strings.NewReader(content), &buf); err != nil {
@ -228,23 +246,10 @@ func postProcessString(ctx *RenderContext, procs []processor, content string) (s
return buf.String(), nil return buf.String(), nil
} }
// RenderDescriptionHTML will use similar logic as PostProcess, but will
// use a single special linkProcessor.
func RenderDescriptionHTML(ctx *RenderContext, content string) (string, error) {
return postProcessString(ctx, []processor{
descriptionLinkProcessor,
emojiShortCodeProcessor,
emojiProcessor,
}, content)
}
// RenderEmoji for when we want to just process emoji and shortcodes
// in various places it isn't already run through the normal markdown processor
func RenderEmoji(ctx *RenderContext, content string) (string, error) {
return postProcessString(ctx, emojiProcessors, content)
}
func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output io.Writer) error { func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output io.Writer) error {
if !ctx.usedByRender && ctx.RenderHelper != nil {
defer ctx.RenderHelper.CleanUp()
}
// FIXME: don't read all content to memory // FIXME: don't read all content to memory
rawHTML, err := io.ReadAll(input) rawHTML, err := io.ReadAll(input)
if err != nil { if err != nil {

View File

@ -252,7 +252,7 @@ func TestRender_IssueIndexPattern_NoShortPattern(t *testing.T) {
testRenderIssueIndexPattern(t, "!1", "!1", NewTestRenderContext(metas)) testRenderIssueIndexPattern(t, "!1", "!1", NewTestRenderContext(metas))
} }
func TestRender_RenderIssueTitle(t *testing.T) { func TestRender_PostProcessIssueTitle(t *testing.T) {
setting.AppURL = TestAppURL setting.AppURL = TestAppURL
metas := map[string]string{ metas := map[string]string{
"format": "https://someurl.com/{user}/{repo}/{index}", "format": "https://someurl.com/{user}/{repo}/{index}",
@ -260,7 +260,7 @@ func TestRender_RenderIssueTitle(t *testing.T) {
"repo": "someRepo", "repo": "someRepo",
"style": IssueNameStyleNumeric, "style": IssueNameStyleNumeric,
} }
actual, err := RenderIssueTitle(NewTestRenderContext(metas), "#1") actual, err := PostProcessIssueTitle(NewTestRenderContext(metas), "#1")
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "#1", actual) assert.Equal(t, "#1", actual)
} }

View File

@ -57,6 +57,9 @@ type RenderOptions struct {
type RenderContext struct { type RenderContext struct {
ctx context.Context ctx context.Context
// the context might be used by the "render" function, but it might also be used by "postProcess" function
usedByRender bool
SidebarTocNode ast.Node SidebarTocNode ast.Node
RenderHelper RenderHelper RenderHelper RenderHelper
@ -182,6 +185,7 @@ func pipes() (io.ReadCloser, io.WriteCloser, func()) {
} }
func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error { func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error {
ctx.usedByRender = true
if ctx.RenderHelper != nil { if ctx.RenderHelper != nil {
defer ctx.RenderHelper.CleanUp() defer ctx.RenderHelper.CleanUp()
} }

View File

@ -38,9 +38,9 @@ func (ut *RenderUtils) RenderCommitMessage(msg string, metas map[string]string)
cleanMsg := template.HTMLEscapeString(msg) cleanMsg := template.HTMLEscapeString(msg)
// we can safely assume that it will not return any error, since there // we can safely assume that it will not return any error, since there
// shouldn't be any special HTML. // shouldn't be any special HTML.
fullMessage, err := markup.RenderCommitMessage(markup.NewRenderContext(ut.ctx).WithMetas(metas), cleanMsg) fullMessage, err := markup.PostProcessCommitMessage(markup.NewRenderContext(ut.ctx).WithMetas(metas), cleanMsg)
if err != nil { if err != nil {
log.Error("RenderCommitMessage: %v", err) log.Error("PostProcessCommitMessage: %v", err)
return "" return ""
} }
msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n") msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n")
@ -65,9 +65,9 @@ func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, me
// we can safely assume that it will not return any error, since there // we can safely assume that it will not return any error, since there
// shouldn't be any special HTML. // shouldn't be any special HTML.
renderedMessage, err := markup.RenderCommitMessageSubject(markup.NewRenderContext(ut.ctx).WithMetas(metas), urlDefault, template.HTMLEscapeString(msgLine)) renderedMessage, err := markup.PostProcessCommitMessageSubject(markup.NewRenderContext(ut.ctx).WithMetas(metas), urlDefault, template.HTMLEscapeString(msgLine))
if err != nil { if err != nil {
log.Error("RenderCommitMessageSubject: %v", err) log.Error("PostProcessCommitMessageSubject: %v", err)
return "" return ""
} }
return renderCodeBlock(template.HTML(renderedMessage)) return renderCodeBlock(template.HTML(renderedMessage))
@ -87,9 +87,9 @@ func (ut *RenderUtils) RenderCommitBody(msg string, metas map[string]string) tem
return "" return ""
} }
renderedMessage, err := markup.RenderCommitMessage(markup.NewRenderContext(ut.ctx).WithMetas(metas), template.HTMLEscapeString(msgLine)) renderedMessage, err := markup.PostProcessCommitMessage(markup.NewRenderContext(ut.ctx).WithMetas(metas), template.HTMLEscapeString(msgLine))
if err != nil { if err != nil {
log.Error("RenderCommitMessage: %v", err) log.Error("PostProcessCommitMessage: %v", err)
return "" return ""
} }
return template.HTML(renderedMessage) return template.HTML(renderedMessage)
@ -106,12 +106,19 @@ func renderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML {
// RenderIssueTitle renders issue/pull title with defined post processors // RenderIssueTitle renders issue/pull title with defined post processors
func (ut *RenderUtils) RenderIssueTitle(text string, metas map[string]string) template.HTML { func (ut *RenderUtils) RenderIssueTitle(text string, metas map[string]string) template.HTML {
renderedText, err := markup.RenderIssueTitle(markup.NewRenderContext(ut.ctx).WithMetas(metas), template.HTMLEscapeString(text)) renderedText, err := markup.PostProcessIssueTitle(markup.NewRenderContext(ut.ctx).WithMetas(metas), template.HTMLEscapeString(text))
if err != nil { if err != nil {
log.Error("RenderIssueTitle: %v", err) log.Error("PostProcessIssueTitle: %v", err)
return "" return ""
} }
return template.HTML(renderedText) return renderCodeBlock(template.HTML(renderedText))
}
// RenderIssueSimpleTitle only renders with emoji and inline code block
func (ut *RenderUtils) RenderIssueSimpleTitle(text string) template.HTML {
ret := ut.RenderEmoji(text)
ret = renderCodeBlock(ret)
return ret
} }
// RenderLabel renders a label // RenderLabel renders a label
@ -174,7 +181,7 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML {
// RenderEmoji renders html text with emoji post processors // RenderEmoji renders html text with emoji post processors
func (ut *RenderUtils) RenderEmoji(text string) template.HTML { func (ut *RenderUtils) RenderEmoji(text string) template.HTML {
renderedText, err := markup.RenderEmoji(markup.NewRenderContext(ut.ctx), template.HTMLEscapeString(text)) renderedText, err := markup.PostProcessEmoji(markup.NewRenderContext(ut.ctx), template.HTMLEscapeString(text))
if err != nil { if err != nil {
log.Error("RenderEmoji: %v", err) log.Error("RenderEmoji: %v", err)
return "" return ""

View File

@ -164,11 +164,11 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
<span class="emoji" aria-label="thumbs up">👍</span> <span class="emoji" aria-label="thumbs up">👍</span>
mail@domain.com mail@domain.com
@mention-user test @mention-user test
#123 <a href="/user13/repo11/issues/123" class="ref-issue">#123</a>
space<SPACE><SPACE> space<SPACE><SPACE>
` `
expected = strings.ReplaceAll(expected, "<SPACE>", " ") expected = strings.ReplaceAll(expected, "<SPACE>", " ")
assert.EqualValues(t, expected, string(newTestRenderUtils().RenderIssueTitle(testInput(), nil))) assert.EqualValues(t, expected, string(newTestRenderUtils().RenderIssueTitle(testInput(), testMetas)))
} }
func TestRenderMarkdownToHtml(t *testing.T) { func TestRenderMarkdownToHtml(t *testing.T) {

View File

@ -9,6 +9,7 @@ import (
"reflect" "reflect"
"strings" "strings"
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
@ -214,7 +215,9 @@ func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Reques
normalizedPath = "/" normalizedPath = "/"
} else if !strings.HasPrefix(normalizedPath+"/", "/v2/") { } else if !strings.HasPrefix(normalizedPath+"/", "/v2/") {
// do not respond to other requests, to simulate a real sub-path environment // do not respond to other requests, to simulate a real sub-path environment
http.Error(resp, "404 page not found, sub-path is: "+setting.AppSubURL, http.StatusNotFound) resp.Header().Add("Content-Type", "text/html; charset=utf-8")
resp.WriteHeader(http.StatusNotFound)
_, _ = resp.Write([]byte(htmlutil.HTMLFormat(`404 page not found, sub-path is: <a href="%s">%s</a>`, setting.AppSubURL, setting.AppSubURL)))
return return
} }
normalized = true normalized = true

View File

@ -1032,6 +1032,8 @@ fork_to_different_account = Fork to a different account
fork_visibility_helper = The visibility of a forked repository cannot be changed. fork_visibility_helper = The visibility of a forked repository cannot be changed.
fork_branch = Branch to be cloned to the fork fork_branch = Branch to be cloned to the fork
all_branches = All branches all_branches = All branches
view_all_branches = View all branches
view_all_tags = View all tags
fork_no_valid_owners = This repository can not be forked because there are no valid owners. fork_no_valid_owners = This repository can not be forked because there are no valid owners.
fork.blocked_user = Cannot fork the repository because you are blocked by the repository owner. fork.blocked_user = Cannot fork the repository because you are blocked by the repository owner.
use_template = Use this template use_template = Use this template
@ -2588,7 +2590,6 @@ diff.generated = generated
diff.vendored = vendored diff.vendored = vendored
diff.comment.add_line_comment = Add line comment diff.comment.add_line_comment = Add line comment
diff.comment.placeholder = Leave a comment diff.comment.placeholder = Leave a comment
diff.comment.markdown_info = Styling with markdown is supported.
diff.comment.add_single_comment = Add single comment diff.comment.add_single_comment = Add single comment
diff.comment.add_review_comment = Add comment diff.comment.add_review_comment = Add comment
diff.comment.start_review = Start review diff.comment.start_review = Start review

View File

@ -610,40 +610,46 @@ func CommonRoutes() *web.Router {
}, reqPackageAccess(perm.AccessModeWrite)) }, reqPackageAccess(perm.AccessModeWrite))
}, reqPackageAccess(perm.AccessModeRead)) }, reqPackageAccess(perm.AccessModeRead))
r.Group("/swift", func() { r.Group("/swift", func() {
r.Group("/{scope}/{name}", func() { r.Group("", func() { // Needs to be unauthenticated.
r.Group("", func() { r.Post("", swift.CheckAuthenticate)
r.Get("", swift.EnumeratePackageVersions) r.Post("/login", swift.CheckAuthenticate)
r.Get(".json", swift.EnumeratePackageVersions) })
}, swift.CheckAcceptMediaType(swift.AcceptJSON)) r.Group("", func() {
r.Group("/{version}", func() { r.Group("/{scope}/{name}", func() {
r.Get("/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest) r.Group("", func() {
r.Put("", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile) r.Get("", swift.EnumeratePackageVersions)
r.Get("", func(ctx *context.Context) { r.Get(".json", swift.EnumeratePackageVersions)
// Can't use normal routes here: https://github.com/go-chi/chi/issues/781 }, swift.CheckAcceptMediaType(swift.AcceptJSON))
r.Group("/{version}", func() {
r.Get("/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest)
r.Put("", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile)
r.Get("", func(ctx *context.Context) {
// Can't use normal routes here: https://github.com/go-chi/chi/issues/781
version := ctx.PathParam("version") version := ctx.PathParam("version")
if strings.HasSuffix(version, ".zip") { if strings.HasSuffix(version, ".zip") {
swift.CheckAcceptMediaType(swift.AcceptZip)(ctx) swift.CheckAcceptMediaType(swift.AcceptZip)(ctx)
if ctx.Written() { if ctx.Written() {
return return
}
ctx.SetPathParam("version", version[:len(version)-4])
swift.DownloadPackageFile(ctx)
} else {
swift.CheckAcceptMediaType(swift.AcceptJSON)(ctx)
if ctx.Written() {
return
}
if strings.HasSuffix(version, ".json") {
ctx.SetPathParam("version", version[:len(version)-5])
}
swift.PackageVersionMetadata(ctx)
} }
ctx.SetPathParam("version", version[:len(version)-4]) })
swift.DownloadPackageFile(ctx)
} else {
swift.CheckAcceptMediaType(swift.AcceptJSON)(ctx)
if ctx.Written() {
return
}
if strings.HasSuffix(version, ".json") {
ctx.SetPathParam("version", version[:len(version)-5])
}
swift.PackageVersionMetadata(ctx)
}
}) })
}) })
}) r.Get("/identifiers", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.LookupPackageIdentifiers)
r.Get("/identifiers", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.LookupPackageIdentifiers) }, reqPackageAccess(perm.AccessModeRead))
}, reqPackageAccess(perm.AccessModeRead)) })
r.Group("/vagrant", func() { r.Group("/vagrant", func() {
r.Group("/authenticate", func() { r.Group("/authenticate", func() {
r.Get("", vagrant.CheckAuthenticate) r.Get("", vagrant.CheckAuthenticate)

View File

@ -27,7 +27,7 @@ import (
"github.com/hashicorp/go-version" "github.com/hashicorp/go-version"
) )
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#35-api-versioning // https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#35-api-versioning
const ( const (
AcceptJSON = "application/vnd.swift.registry.v1+json" AcceptJSON = "application/vnd.swift.registry.v1+json"
AcceptSwift = "application/vnd.swift.registry.v1+swift" AcceptSwift = "application/vnd.swift.registry.v1+swift"
@ -35,9 +35,9 @@ const (
) )
var ( var (
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#361-package-scope // https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#361-package-scope
scopePattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9-]{0,38}\z`) scopePattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9-]{0,38}\z`)
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#362-package-name // https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#362-package-name
namePattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9-_]{0,99}\z`) namePattern = regexp.MustCompile(`\A[a-zA-Z0-9][a-zA-Z0-9-_]{0,99}\z`)
) )
@ -49,7 +49,7 @@ type headers struct {
Link string Link string
} }
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#35-api-versioning // https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#35-api-versioning
func setResponseHeaders(resp http.ResponseWriter, h *headers) { func setResponseHeaders(resp http.ResponseWriter, h *headers) {
if h.ContentType != "" { if h.ContentType != "" {
resp.Header().Set("Content-Type", h.ContentType) resp.Header().Set("Content-Type", h.ContentType)
@ -69,7 +69,7 @@ func setResponseHeaders(resp http.ResponseWriter, h *headers) {
} }
} }
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#33-error-handling // https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#33-error-handling
func apiError(ctx *context.Context, status int, obj any) { func apiError(ctx *context.Context, status int, obj any) {
// https://www.rfc-editor.org/rfc/rfc7807 // https://www.rfc-editor.org/rfc/rfc7807
type Problem struct { type Problem struct {
@ -91,7 +91,7 @@ func apiError(ctx *context.Context, status int, obj any) {
}) })
} }
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#35-api-versioning // https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#35-api-versioning
func CheckAcceptMediaType(requiredAcceptHeader string) func(ctx *context.Context) { func CheckAcceptMediaType(requiredAcceptHeader string) func(ctx *context.Context) {
return func(ctx *context.Context) { return func(ctx *context.Context) {
accept := ctx.Req.Header.Get("Accept") accept := ctx.Req.Header.Get("Accept")
@ -101,6 +101,16 @@ func CheckAcceptMediaType(requiredAcceptHeader string) func(ctx *context.Context
} }
} }
// https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/PackageRegistryUsage.md#registry-authentication
func CheckAuthenticate(ctx *context.Context) {
if ctx.Doer == nil {
apiError(ctx, http.StatusUnauthorized, nil)
return
}
ctx.Status(http.StatusOK)
}
func buildPackageID(scope, name string) string { func buildPackageID(scope, name string) string {
return scope + "." + name return scope + "." + name
} }
@ -113,7 +123,7 @@ type EnumeratePackageVersionsResponse struct {
Releases map[string]Release `json:"releases"` Releases map[string]Release `json:"releases"`
} }
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#41-list-package-releases // https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#41-list-package-releases
func EnumeratePackageVersions(ctx *context.Context) { func EnumeratePackageVersions(ctx *context.Context) {
packageScope := ctx.PathParam("scope") packageScope := ctx.PathParam("scope")
packageName := ctx.PathParam("name") packageName := ctx.PathParam("name")
@ -170,7 +180,7 @@ type PackageVersionMetadataResponse struct {
Metadata *swift_module.SoftwareSourceCode `json:"metadata"` Metadata *swift_module.SoftwareSourceCode `json:"metadata"`
} }
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-2 // https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-2
func PackageVersionMetadata(ctx *context.Context) { func PackageVersionMetadata(ctx *context.Context) {
id := buildPackageID(ctx.PathParam("scope"), ctx.PathParam("name")) id := buildPackageID(ctx.PathParam("scope"), ctx.PathParam("name"))
@ -228,7 +238,7 @@ func PackageVersionMetadata(ctx *context.Context) {
}) })
} }
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#43-fetch-manifest-for-a-package-release // https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#43-fetch-manifest-for-a-package-release
func DownloadManifest(ctx *context.Context) { func DownloadManifest(ctx *context.Context) {
packageScope := ctx.PathParam("scope") packageScope := ctx.PathParam("scope")
packageName := ctx.PathParam("name") packageName := ctx.PathParam("name")
@ -280,7 +290,7 @@ func DownloadManifest(ctx *context.Context) {
}) })
} }
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-6 // https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-6
func UploadPackageFile(ctx *context.Context) { func UploadPackageFile(ctx *context.Context) {
packageScope := ctx.PathParam("scope") packageScope := ctx.PathParam("scope")
packageName := ctx.PathParam("name") packageName := ctx.PathParam("name")
@ -379,7 +389,7 @@ func UploadPackageFile(ctx *context.Context) {
ctx.Status(http.StatusCreated) ctx.Status(http.StatusCreated)
} }
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-4 // https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-4
func DownloadPackageFile(ctx *context.Context) { func DownloadPackageFile(ctx *context.Context) {
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, buildPackageID(ctx.PathParam("scope"), ctx.PathParam("name")), ctx.PathParam("version")) pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeSwift, buildPackageID(ctx.PathParam("scope"), ctx.PathParam("name")), ctx.PathParam("version"))
if err != nil { if err != nil {
@ -420,7 +430,7 @@ type LookupPackageIdentifiersResponse struct {
Identifiers []string `json:"identifiers"` Identifiers []string `json:"identifiers"`
} }
// https://github.com/apple/swift-package-manager/blob/main/Documentation/Registry.md#endpoint-5 // https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#endpoint-5
func LookupPackageIdentifiers(ctx *context.Context) { func LookupPackageIdentifiers(ctx *context.Context) {
url := ctx.FormTrim("url") url := ctx.FormTrim("url")
if url == "" { if url == "" {

View File

@ -150,11 +150,6 @@ func DeleteBranch(ctx *context.APIContext) {
} }
} }
if ctx.Repo.Repository.IsMirror {
ctx.Error(http.StatusForbidden, "IsMirrored", fmt.Errorf("can not delete branch of an mirror repository"))
return
}
if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil { if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil {
switch { switch {
case git.IsErrBranchNotExist(err): case git.IsErrBranchNotExist(err):

View File

@ -1057,49 +1057,54 @@ func MergePullRequest(ctx *context.APIContext) {
} }
log.Trace("Pull request merged: %d", pr.ID) log.Trace("Pull request merged: %d", pr.ID)
if form.DeleteBranchAfterMerge { // for agit flow, we should not delete the agit reference after merge
// Don't cleanup when there are other PR's that use this branch as head branch. if form.DeleteBranchAfterMerge && pr.Flow == issues_model.PullRequestFlowGithub {
exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch) // check permission even it has been checked in repo_service.DeleteBranch so that we don't need to
if err != nil { // do RetargetChildrenOnMerge
ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err) if err := repo_service.CanDeleteBranch(ctx, pr.HeadRepo, pr.HeadBranch, ctx.Doer); err == nil {
return // Don't cleanup when there are other PR's that use this branch as head branch.
} exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
if exist {
ctx.Status(http.StatusOK)
return
}
var headRepo *git.Repository
if ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == pr.HeadRepoID && ctx.Repo.GitRepo != nil {
headRepo = ctx.Repo.GitRepo
} else {
headRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo)
if err != nil { if err != nil {
ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.FullName()), err) ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
return return
} }
defer headRepo.Close() if exist {
} ctx.Status(http.StatusOK)
if err := pull_service.RetargetChildrenOnMerge(ctx, ctx.Doer, pr); err != nil { return
ctx.Error(http.StatusInternalServerError, "RetargetChildrenOnMerge", err) }
return
} var headRepo *git.Repository
if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, headRepo, pr.HeadBranch); err != nil { if ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == pr.HeadRepoID && ctx.Repo.GitRepo != nil {
switch { headRepo = ctx.Repo.GitRepo
case git.IsErrBranchNotExist(err): } else {
ctx.NotFound(err) headRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo)
case errors.Is(err, repo_service.ErrBranchIsDefault): if err != nil {
ctx.Error(http.StatusForbidden, "DefaultBranch", fmt.Errorf("can not delete default branch")) ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.FullName()), err)
case errors.Is(err, git_model.ErrBranchIsProtected): return
ctx.Error(http.StatusForbidden, "IsProtectedBranch", fmt.Errorf("branch protected")) }
default: defer headRepo.Close()
ctx.Error(http.StatusInternalServerError, "DeleteBranch", err) }
if err := pull_service.RetargetChildrenOnMerge(ctx, ctx.Doer, pr); err != nil {
ctx.Error(http.StatusInternalServerError, "RetargetChildrenOnMerge", err)
return
}
if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, headRepo, pr.HeadBranch); err != nil {
switch {
case git.IsErrBranchNotExist(err):
ctx.NotFound(err)
case errors.Is(err, repo_service.ErrBranchIsDefault):
ctx.Error(http.StatusForbidden, "DefaultBranch", fmt.Errorf("can not delete default branch"))
case errors.Is(err, git_model.ErrBranchIsProtected):
ctx.Error(http.StatusForbidden, "IsProtectedBranch", fmt.Errorf("branch protected"))
default:
ctx.Error(http.StatusInternalServerError, "DeleteBranch", err)
}
return
}
if err := issues_model.AddDeletePRBranchComment(ctx, ctx.Doer, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil {
// Do not fail here as branch has already been deleted
log.Error("DeleteBranch: %v", err)
} }
return
}
if err := issues_model.AddDeletePRBranchComment(ctx, ctx.Doer, pr.BaseRepo, pr.Issue.ID, pr.HeadBranch); err != nil {
// Do not fail here as branch has already been deleted
log.Error("DeleteBranch: %v", err)
} }
} }

View File

@ -394,9 +394,9 @@ func Diff(ctx *context.Context) {
ctx.Data["NoteCommit"] = note.Commit ctx.Data["NoteCommit"] = note.Commit
ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit) ctx.Data["NoteAuthor"] = user_model.ValidateCommitWithEmail(ctx, note.Commit)
rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{CurrentRefPath: path.Join("commit", util.PathEscapeSegments(commitID))}) rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{CurrentRefPath: path.Join("commit", util.PathEscapeSegments(commitID))})
ctx.Data["NoteRendered"], err = markup.RenderCommitMessage(rctx, template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{})))) ctx.Data["NoteRendered"], err = markup.PostProcessCommitMessage(rctx, template.HTMLEscapeString(string(charset.ToUTF8WithFallback(note.Message, charset.ConvertOpts{}))))
if err != nil { if err != nil {
ctx.ServerError("RenderCommitMessage", err) ctx.ServerError("PostProcessCommitMessage", err)
return return
} }
} }

View File

@ -1186,32 +1186,34 @@ func MergePullRequest(ctx *context.Context) {
log.Trace("Pull request merged: %d", pr.ID) log.Trace("Pull request merged: %d", pr.ID)
if form.DeleteBranchAfterMerge { if !form.DeleteBranchAfterMerge {
// Don't cleanup when other pr use this branch as head branch ctx.JSONRedirect(issue.Link())
exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch) return
if err != nil {
ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
return
}
if exist {
ctx.JSONRedirect(issue.Link())
return
}
var headRepo *git.Repository
if ctx.Repo != nil && ctx.Repo.Repository != nil && pr.HeadRepoID == ctx.Repo.Repository.ID && ctx.Repo.GitRepo != nil {
headRepo = ctx.Repo.GitRepo
} else {
headRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo)
if err != nil {
ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.FullName()), err)
return
}
defer headRepo.Close()
}
deleteBranch(ctx, pr, headRepo)
} }
// Don't cleanup when other pr use this branch as head branch
exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
if err != nil {
ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
return
}
if exist {
ctx.JSONRedirect(issue.Link())
return
}
var headRepo *git.Repository
if ctx.Repo != nil && ctx.Repo.Repository != nil && pr.HeadRepoID == ctx.Repo.Repository.ID && ctx.Repo.GitRepo != nil {
headRepo = ctx.Repo.GitRepo
} else {
headRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo)
if err != nil {
ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.FullName()), err)
return
}
defer headRepo.Close()
}
deleteBranch(ctx, pr, headRepo)
ctx.JSONRedirect(issue.Link()) ctx.JSONRedirect(issue.Link())
} }
@ -1404,8 +1406,8 @@ func CleanUpPullRequest(ctx *context.Context) {
pr := issue.PullRequest pr := issue.PullRequest
// Don't cleanup unmerged and unclosed PRs // Don't cleanup unmerged and unclosed PRs and agit PRs
if !pr.HasMerged && !issue.IsClosed { if !pr.HasMerged && !issue.IsClosed && pr.Flow != issues_model.PullRequestFlowGithub {
ctx.NotFound("CleanUpPullRequest", nil) ctx.NotFound("CleanUpPullRequest", nil)
return return
} }
@ -1436,13 +1438,12 @@ func CleanUpPullRequest(ctx *context.Context) {
return return
} }
perm, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, ctx.Doer) if err := repo_service.CanDeleteBranch(ctx, pr.HeadRepo, pr.HeadBranch, ctx.Doer); err != nil {
if err != nil { if errors.Is(err, util.ErrPermissionDenied) {
ctx.ServerError("GetUserRepoPermission", err) ctx.NotFound("CanDeleteBranch", nil)
return } else {
} ctx.ServerError("CanDeleteBranch", err)
if !perm.CanWrite(unit.TypeCode) { }
ctx.NotFound("CleanUpPullRequest", nil)
return return
} }

View File

@ -31,6 +31,7 @@ import (
"code.gitea.io/gitea/services/context/upload" "code.gitea.io/gitea/services/context/upload"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
releaseservice "code.gitea.io/gitea/services/release" releaseservice "code.gitea.io/gitea/services/release"
repo_service "code.gitea.io/gitea/services/repository"
) )
const ( const (
@ -193,6 +194,8 @@ func Releases(ctx *context.Context) {
pager.SetDefaultParams(ctx) pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager ctx.Data["Page"] = pager
ctx.Data["LicenseFileName"] = repo_service.LicenseFileName
ctx.HTML(http.StatusOK, tplReleasesList) ctx.HTML(http.StatusOK, tplReleasesList)
} }
@ -251,6 +254,7 @@ func TagsList(ctx *context.Context) {
pager.SetDefaultParams(ctx) pager.SetDefaultParams(ctx)
ctx.Data["Page"] = pager ctx.Data["Page"] = pager
ctx.Data["PageIsViewCode"] = !ctx.Repo.Repository.UnitEnabled(ctx, unit.TypeReleases) ctx.Data["PageIsViewCode"] = !ctx.Repo.Repository.UnitEnabled(ctx, unit.TypeReleases)
ctx.Data["LicenseFileName"] = repo_service.LicenseFileName
ctx.HTML(http.StatusOK, tplTagsList) ctx.HTML(http.StatusOK, tplTagsList)
} }

View File

@ -485,6 +485,8 @@ func registerRoutes(m *web.Router) {
m.Methods("GET, HEAD", "/*", public.FileHandlerFunc()) m.Methods("GET, HEAD", "/*", public.FileHandlerFunc())
}, optionsCorsHandler()) }, optionsCorsHandler())
m.Post("/-/markup", reqSignIn, web.Bind(structs.MarkupOption{}), misc.Markup)
m.Group("/explore", func() { m.Group("/explore", func() {
m.Get("", func(ctx *context.Context) { m.Get("", func(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/explore/repos") ctx.Redirect(setting.AppSubURL + "/explore/repos")

View File

@ -14,7 +14,9 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
@ -463,15 +465,17 @@ var (
ErrBranchIsDefault = errors.New("branch is default") ErrBranchIsDefault = errors.New("branch is default")
) )
// DeleteBranch delete branch func CanDeleteBranch(ctx context.Context, repo *repo_model.Repository, branchName string, doer *user_model.User) error {
func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, branchName string) error { if branchName == repo.DefaultBranch {
err := repo.MustNotBeArchived() return ErrBranchIsDefault
}
perm, err := access_model.GetUserRepoPermission(ctx, repo, doer)
if err != nil { if err != nil {
return err return err
} }
if !perm.CanWrite(unit.TypeCode) {
if branchName == repo.DefaultBranch { return util.NewPermissionDeniedErrorf("permission denied to access repo %d unit %s", repo.ID, unit.TypeCode.LogString())
return ErrBranchIsDefault
} }
isProtected, err := git_model.IsBranchProtected(ctx, repo.ID, branchName) isProtected, err := git_model.IsBranchProtected(ctx, repo.ID, branchName)
@ -481,6 +485,19 @@ func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.R
if isProtected { if isProtected {
return git_model.ErrBranchIsProtected return git_model.ErrBranchIsProtected
} }
return nil
}
// DeleteBranch delete branch
func DeleteBranch(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, branchName string) error {
err := repo.MustNotBeArchived()
if err != nil {
return err
}
if err := CanDeleteBranch(ctx, repo, branchName, doer); err != nil {
return err
}
rawBranch, err := git_model.GetBranch(ctx, repo.ID, branchName) rawBranch, err := git_model.GetBranch(ctx, repo.ID, branchName)
if err != nil && !git_model.IsErrBranchNotExist(err) { if err != nil && !git_model.IsErrBranchNotExist(err) {

View File

@ -7,10 +7,12 @@
</div> </div>
</h4> </h4>
<div class="ui attached segment"> <div class="ui attached segment">
<form class="ui form ignore-dirty" id="user-list-search-form"> <form class="ui form ignore-dirty flex-text-block" id="user-list-search-form">
<div class="tw-flex-1">
{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.user_kind")}}
</div>
<!-- Right Menu --> <!-- Right Menu -->
<div class="ui right floated secondary filter menu"> <div class="ui secondary menu tw-m-0">
<!-- Status Filter Menu Item --> <!-- Status Filter Menu Item -->
<div class="ui dropdown type jump item"> <div class="ui dropdown type jump item">
<span class="text">{{ctx.Locale.Tr "admin.users.list_status_filter.menu_text"}}</span> <span class="text">{{ctx.Locale.Tr "admin.users.list_status_filter.menu_text"}}</span>
@ -51,8 +53,6 @@
</div> </div>
</div> </div>
</div> </div>
{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.user_kind")}}
</form> </form>
</div> </div>
<div class="ui attached table segment"> <div class="ui attached table segment">

View File

@ -1,3 +1,3 @@
{{/* TODO: the devtest.js is isolated from index.js, so no module is shared and many index.js functions do not work in devtest.ts */}} {{/* TODO: the devtest.js is isolated from index.js, so no module is shared and many index.js functions do not work in devtest.ts */}}
<script src="{{AssetUrlPrefix}}/js/devtest.js?v={{AssetVersion}}"></script> <script src="{{AssetUrlPrefix}}/js/devtest.js?v={{AssetVersion}}"></script>
{{template "base/footer" dict}} {{template "base/footer" ctx.RootData}}

View File

@ -1,2 +1,2 @@
{{template "base/head" dict}} {{template "base/head" ctx.RootData}}
<link rel="stylesheet" href="{{AssetUrlPrefix}}/css/devtest.css?v={{AssetVersion}}"> <link rel="stylesheet" href="{{AssetUrlPrefix}}/css/devtest.css?v={{AssetVersion}}">

View File

@ -183,8 +183,7 @@
<div> <div>
<h1>ComboMarkdownEditor</h1> <h1>ComboMarkdownEditor</h1>
<div>ps: no JS code attached, so just a layout</div> {{template "shared/combomarkdowneditor" dict "MarkdownPreviewContext" "/owner/path"}}
{{template "shared/combomarkdowneditor" .}}
</div> </div>
<h1>Tailwind CSS Demo</h1> <h1>Tailwind CSS Demo</h1>

View File

@ -23,6 +23,7 @@
<input id="email" name="email" type="email" value="{{.Org.Email}}" maxlength="255"> <input id="email" name="email" type="email" value="{{.Org.Email}}" maxlength="255">
</div> </div>
<div class="field {{if .Err_Description}}error{{end}}"> <div class="field {{if .Err_Description}}error{{end}}">
{{/* it is rendered as markdown, but the length is limited, so at the moment we do not use the markdown editor here */}}
<label for="description">{{ctx.Locale.Tr "org.org_desc"}}</label> <label for="description">{{ctx.Locale.Tr "org.org_desc"}}</label>
<textarea id="description" name="description" rows="2" maxlength="255">{{.Org.Description}}</textarea> <textarea id="description" name="description" rows="2" maxlength="255">{{.Org.Description}}</textarea>
</div> </div>

View File

@ -18,7 +18,16 @@
</div> </div>
<div class="field"> <div class="field">
<label>{{ctx.Locale.Tr "repo.projects.description"}}</label> <label>{{ctx.Locale.Tr "repo.projects.description"}}</label>
<textarea name="content" placeholder="{{ctx.Locale.Tr "repo.projects.description_placeholder"}}">{{.content}}</textarea> {{/* TODO: repo-level project and org-level project have different behaviros to render */}}
{{/* the "Repository" is nil when the project is org-level */}}
{{template "shared/combomarkdowneditor" (dict
"MarkdownPreviewInRepo" $.Repository
"MarkdownPreviewContext" (Iif $.Repository "" .HomeLink)
"MarkdownPreviewMode" (Iif $.Repository "comment")
"TextareaName" "content"
"TextareaContent" .content
"TextareaPlaceholder" (ctx.Locale.Tr "repo.projects.description_placeholder")
)}}
</div> </div>
{{if not .PageIsEditProjects}} {{if not .PageIsEditProjects}}

View File

@ -10,6 +10,7 @@
* ShowTabBranches * ShowTabBranches
* ShowTabTagsTab * ShowTabTagsTab
* AllowCreateNewRef * AllowCreateNewRef
* ShowViewAllRefsEntry
Search "repo/branch_dropdown" in the template directory to find all occurrences. Search "repo/branch_dropdown" in the template directory to find all occurrences.
*/}} */}}
@ -24,6 +25,8 @@ Search "repo/branch_dropdown" in the template directory to find all occurrences.
data-text-create-branch="{{ctx.Locale.Tr "repo.branch.create_branch"}}" data-text-create-branch="{{ctx.Locale.Tr "repo.branch.create_branch"}}"
data-text-create-ref-from="{{ctx.Locale.Tr "repo.branch.create_from"}}" data-text-create-ref-from="{{ctx.Locale.Tr "repo.branch.create_from"}}"
data-text-no-results="{{ctx.Locale.Tr "no_results_found"}}" data-text-no-results="{{ctx.Locale.Tr "no_results_found"}}"
data-text-view-all-branches="{{ctx.Locale.Tr "repo.view_all_branches"}}"
data-text-view-all-tags="{{ctx.Locale.Tr "repo.view_all_tags"}}"
data-current-repo-default-branch="{{.Repository.DefaultBranch}}" data-current-repo-default-branch="{{.Repository.DefaultBranch}}"
data-current-repo-link="{{.Repository.Link}}" data-current-repo-link="{{.Repository.Link}}"
@ -37,6 +40,7 @@ Search "repo/branch_dropdown" in the template directory to find all occurrences.
data-show-tab-branches="{{.ShowTabBranches}}" data-show-tab-branches="{{.ShowTabBranches}}"
data-show-tab-tags="{{.ShowTabTags}}" data-show-tab-tags="{{.ShowTabTags}}"
data-allow-create-new-ref="{{.AllowCreateNewRef}}" data-allow-create-new-ref="{{.AllowCreateNewRef}}"
data-show-view-all-refs-entry="{{.ShowViewAllRefsEntry}}"
data-enable-feed="{{ctx.RootData.EnableFeed}}" data-enable-feed="{{ctx.RootData.EnableFeed}}"
> >

View File

@ -240,8 +240,9 @@
<template id="issue-comment-editor-template"> <template id="issue-comment-editor-template">
<div class="ui form comment"> <div class="ui form comment">
{{template "shared/combomarkdowneditor" (dict {{template "shared/combomarkdowneditor" (dict
"MarkdownPreviewUrl" (print $.Repository.Link "/markup") "CustomInit" true
"MarkdownPreviewContext" $.RepoLink "MarkdownPreviewInRepo" $.Repository
"MarkdownPreviewMode" "comment"
"TextareaName" "content" "TextareaName" "content"
"DropzoneParentContainer" ".ui.form" "DropzoneParentContainer" ".ui.form"
)}} )}}

View File

@ -9,24 +9,24 @@
<input type="hidden" name="diff_start_cid"> <input type="hidden" name="diff_start_cid">
<input type="hidden" name="diff_end_cid"> <input type="hidden" name="diff_end_cid">
<input type="hidden" name="diff_base_cid"> <input type="hidden" name="diff_base_cid">
<div class="field">
{{template "shared/combomarkdowneditor" (dict {{template "shared/combomarkdowneditor" (dict
"MarkdownPreviewUrl" (print $.root.Repository.Link "/markup") "CustomInit" true
"MarkdownPreviewContext" $.root.RepoLink "MarkdownPreviewInRepo" $.root.Repository
"MarkdownPreviewMode" "comment"
"TextareaName" "content" "TextareaName" "content"
"TextareaPlaceholder" (ctx.Locale.Tr "repo.diff.comment.placeholder") "TextareaPlaceholder" (ctx.Locale.Tr "repo.diff.comment.placeholder")
"DropzoneParentContainer" "form" "DropzoneParentContainer" "form"
"DisableAutosize" "true" "DisableAutosize" "true"
)}} )}}
</div>
{{if $.root.IsAttachmentEnabled}} {{if $.root.IsAttachmentEnabled}}
<div class="field"> <div class="field">
{{template "repo/upload" $.root}} {{template "repo/upload" $.root}}
</div> </div>
{{end}} {{end}}
<div class="field footer tw-mx-2"> <div class="field footer">
<span class="markup-info">{{svg "octicon-markdown"}} {{ctx.Locale.Tr "repo.diff.comment.markdown_info"}}</span>
<div class="tw-text-right"> <div class="tw-text-right">
{{if $.reply}} {{if $.reply}}
<button class="ui submit primary tiny button btn-reply" type="submit">{{ctx.Locale.Tr "repo.diff.comment.reply"}}</button> <button class="ui submit primary tiny button btn-reply" type="submit">{{ctx.Locale.Tr "repo.diff.comment.reply"}}</button>

View File

@ -187,7 +187,7 @@
<div class="ui segment flex-text-block tw-gap-4"> <div class="ui segment flex-text-block tw-gap-4">
{{template "shared/issueicon" .}} {{template "shared/issueicon" .}}
<div class="issue-title tw-break-anywhere"> <div class="issue-title tw-break-anywhere">
{{ctx.RenderUtils.RenderIssueTitle .PullRequest.Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}} {{ctx.RenderUtils.RenderIssueTitle .PullRequest.Issue.Title ($.Repository.ComposeMetas ctx)}}
<span class="index">#{{.PullRequest.Issue.Index}}</span> <span class="index">#{{.PullRequest.Issue.Index}}</span>
</div> </div>
<a href="{{$.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui compact button primary"> <a href="{{$.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui compact button primary">

View File

@ -16,8 +16,8 @@
</div> </div>
<div class="field"> <div class="field">
{{template "shared/combomarkdowneditor" (dict {{template "shared/combomarkdowneditor" (dict
"MarkdownPreviewUrl" (print .Repository.Link "/markup") "MarkdownPreviewInRepo" $.Repository
"MarkdownPreviewContext" .RepoLink "MarkdownPreviewMode" "comment"
"TextareaName" "content" "TextareaName" "content"
"TextareaPlaceholder" (ctx.Locale.Tr "repo.diff.review.placeholder") "TextareaPlaceholder" (ctx.Locale.Tr "repo.diff.review.placeholder")
"DropzoneParentContainer" "form" "DropzoneParentContainer" "form"

View File

@ -3,7 +3,10 @@
{{template "repo/header" .}} {{template "repo/header" .}}
<div class="ui container"> <div class="ui container">
{{template "base/alert" .}} {{template "base/alert" .}}
<form class="ui edit form" method="post"> <form class="ui edit form" method="post"
data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}"
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
>
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<input type="hidden" name="last_commit" value="{{.last_commit}}"> <input type="hidden" name="last_commit" value="{{.last_commit}}">
<input type="hidden" name="page_has_posted" value="{{.PageHasPosted}}"> <input type="hidden" name="page_has_posted" value="{{.PageHasPosted}}">
@ -29,7 +32,7 @@
<div class="ui top attached header"> <div class="ui top attached header">
<div class="ui compact small menu small-menu-items repo-editor-menu"> <div class="ui compact small menu small-menu-items repo-editor-menu">
<a class="active item" data-tab="write">{{svg "octicon-code"}} {{if .IsNewFile}}{{ctx.Locale.Tr "repo.editor.new_file"}}{{else}}{{ctx.Locale.Tr "repo.editor.edit_file"}}{{end}}</a> <a class="active item" data-tab="write">{{svg "octicon-code"}} {{if .IsNewFile}}{{ctx.Locale.Tr "repo.editor.new_file"}}{{else}}{{ctx.Locale.Tr "repo.editor.edit_file"}}{{end}}</a>
<a class="item" data-tab="preview" data-url="{{.Repository.Link}}/markup" data-context="{{.RepoLink}}/src/{{.BranchNameSubURL}}" data-markup-mode="file">{{svg "octicon-eye"}} {{ctx.Locale.Tr "preview"}}</a> <a class="item" data-tab="preview" data-preview-url="{{.Repository.Link}}/markup" data-preview-context-ref="{{.RepoLink}}/src/{{.BranchNameSubURL}}">{{svg "octicon-eye"}} {{ctx.Locale.Tr "preview"}}</a>
{{if not .IsNewFile}} {{if not .IsNewFile}}
<a class="item" data-tab="diff" hx-params="context,content" hx-vals='{"context":"{{.BranchLink}}"}' hx-include="#edit_area" hx-swap="innerHTML" hx-target=".tab[data-tab='diff']" hx-indicator=".tab[data-tab='diff']" hx-post="{{.RepoLink}}/_preview/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">{{svg "octicon-diff"}} {{ctx.Locale.Tr "repo.editor.preview_changes"}}</a> <a class="item" data-tab="diff" hx-params="context,content" hx-vals='{"context":"{{.BranchLink}}"}' hx-include="#edit_area" hx-swap="innerHTML" hx-target=".tab[data-tab='diff']" hx-indicator=".tab[data-tab='diff']" hx-post="{{.RepoLink}}/_preview/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">{{svg "octicon-diff"}} {{ctx.Locale.Tr "repo.editor.preview_changes"}}</a>
{{end}} {{end}}
@ -38,8 +41,6 @@
<div class="ui bottom attached segment tw-p-0"> <div class="ui bottom attached segment tw-p-0">
<div class="ui active tab tw-rounded-b" data-tab="write"> <div class="ui active tab tw-rounded-b" data-tab="write">
<textarea id="edit_area" name="content" class="tw-hidden" data-id="repo-{{.Repository.Name}}-{{.TreePath}}" <textarea id="edit_area" name="content" class="tw-hidden" data-id="repo-{{.Repository.Name}}-{{.TreePath}}"
data-url="{{.Repository.Link}}/markup"
data-context="{{.RepoLink}}"
data-previewable-extensions="{{.PreviewableExtensions}}" data-previewable-extensions="{{.PreviewableExtensions}}"
data-line-wrap-extensions="{{.LineWrapExtensions}}">{{.FileContent}}</textarea> data-line-wrap-extensions="{{.LineWrapExtensions}}">{{.FileContent}}</textarea>
<div class="editor-loading is-loading"></div> <div class="editor-loading is-loading"></div>
@ -55,24 +56,5 @@
{{template "repo/editor/commit_form" .}} {{template "repo/editor/commit_form" .}}
</form> </form>
</div> </div>
<div class="ui g-modal-confirm modal" id="edit-empty-content-modal">
<div class="header">
{{svg "octicon-file"}}
{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}
</div>
<div class="center content">
<p>{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}</p>
</div>
<div class="actions">
<button class="ui cancel button">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "repo.editor.cancel"}}
</button>
<button class="ui primary ok button">
{{svg "fontawesome-save"}}
{{ctx.Locale.Tr "repo.editor.commit_changes"}}
</button>
</div>
</div>
</div> </div>
{{template "base/footer" .}} {{template "base/footer" .}}

View File

@ -3,7 +3,10 @@
{{template "repo/header" .}} {{template "repo/header" .}}
<div class="ui container"> <div class="ui container">
{{template "base/alert" .}} {{template "base/alert" .}}
<form class="ui edit form" method="post" action="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}"> <form class="ui edit form" method="post" action="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}"
data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}"
data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}"
>
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
<input type="hidden" name="last_commit" value="{{.last_commit}}"> <input type="hidden" name="last_commit" value="{{.last_commit}}">
<input type="hidden" name="page_has_posted" value="{{.PageHasPosted}}"> <input type="hidden" name="page_has_posted" value="{{.PageHasPosted}}">
@ -33,25 +36,5 @@
{{template "repo/editor/commit_form" .}} {{template "repo/editor/commit_form" .}}
</form> </form>
</div> </div>
<div class="ui g-modal-confirm modal" id="edit-empty-content-modal">
<div class="header">
{{svg "octicon-file"}}
{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}
</div>
<div class="center content">
<p>{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}</p>
</div>
<div class="actions">
<button class="ui cancel button">
{{svg "octicon-x"}}
{{ctx.Locale.Tr "repo.editor.cancel"}}
</button>
<button class="ui primary ok button">
{{svg "fontawesome-save"}}
{{ctx.Locale.Tr "repo.editor.commit_changes"}}
</button>
</div>
</div>
</div> </div>
{{template "base/footer" .}} {{template "base/footer" .}}

View File

@ -9,7 +9,7 @@
<input id="repo-file-find-input" type="text" autofocus data-url-data-link="{{.DataLink}}" data-url-tree-link="{{.TreeLink}}"> <input id="repo-file-find-input" type="text" autofocus data-url-data-link="{{.DataLink}}" data-url-tree-link="{{.TreeLink}}">
</div> </div>
</div> </div>
<table id="repo-find-file-table" class="ui single line table"> <table id="repo-find-file-table" class="ui single line fixed table">
<tbody> <tbody>
</tbody> </tbody>
</table> </table>

View File

@ -62,6 +62,7 @@
"CurrentTreePath" .TreePath "CurrentTreePath" .TreePath
"RefLinkTemplate" "{RepoLink}/src/{RefType}/{RefShortName}/{TreePath}" "RefLinkTemplate" "{RepoLink}/src/{RefType}/{RefShortName}/{TreePath}"
"AllowCreateNewRef" .CanCreateBranch "AllowCreateNewRef" .CanCreateBranch
"ShowViewAllRefsEntry" true
}} }}
{{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}} {{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}}
{{$cmpBranch := ""}} {{$cmpBranch := ""}}

View File

@ -14,7 +14,7 @@
<div class="issue-card-icon"> <div class="issue-card-icon">
{{template "shared/issueicon" .}} {{template "shared/issueicon" .}}
</div> </div>
<a class="issue-card-title muted issue-title tw-break-anywhere" href="{{.Link}}">{{.Title | ctx.RenderUtils.RenderEmoji | RenderCodeBlock}}</a> <a class="issue-card-title muted issue-title tw-break-anywhere" href="{{.Link}}">{{.Title | ctx.RenderUtils.RenderIssueSimpleTitle}}</a>
{{if and $.isPinnedIssueCard $.Page.IsRepoAdmin}} {{if and $.isPinnedIssueCard $.Page.IsRepoAdmin}}
<a role="button" class="issue-card-unpin muted tw-flex tw-items-center" data-tooltip-content={{ctx.Locale.Tr "repo.issues.unpin_issue"}} data-issue-id="{{.ID}}" data-unpin-url="{{$.Page.Link}}/unpin/{{.Index}}"> <a role="button" class="issue-card-unpin muted tw-flex tw-items-center" data-tooltip-content={{ctx.Locale.Tr "repo.issues.unpin_issue"}} data-issue-id="{{.ID}}" data-unpin-url="{{$.Page.Link}}/unpin/{{.Index}}">
{{svg "octicon-x" 16}} {{svg "octicon-x" 16}}

View File

@ -5,11 +5,12 @@
<div class="field"> <div class="field">
{{template "shared/combomarkdowneditor" (dict {{template "shared/combomarkdowneditor" (dict
"MarkdownPreviewUrl" (print .Repository.Link "/markup") "CustomInit" true
"MarkdownPreviewContext" .RepoLink "MarkdownPreviewInRepo" $.Repository
"MarkdownPreviewMode" "comment"
"TextareaName" "content" "TextareaName" "content"
"TextareaContent" $textareaContent "TextareaContent" $textareaContent
"TextareaPlaceholder" (ctx.Locale.Tr "repo.diff.comment.placeholder") "TextareaPlaceholder" (ctx.Locale.Tr "repo.diff.comment.placeholder")
"DropzoneParentContainer" "form, .ui.form" "DropzoneParentContainer" "form, .ui.form"
)}} )}}
</div> </div>

View File

@ -7,11 +7,12 @@
{{if $useMarkdownEditor}} {{if $useMarkdownEditor}}
{{template "shared/combomarkdowneditor" (dict {{template "shared/combomarkdowneditor" (dict
"CustomInit" true
"ContainerClasses" "tw-hidden" "ContainerClasses" "tw-hidden"
"MarkdownPreviewUrl" (print .root.RepoLink "/markup") "MarkdownPreviewInRepo" $.root.Repository
"MarkdownPreviewContext" .root.RepoLink "MarkdownPreviewMode" "comment"
"TextareaContent" .item.Attributes.value "TextareaContent" .item.Attributes.value
"TextareaPlaceholder" .item.Attributes.placeholder "TextareaPlaceholder" .item.Attributes.placeholder
"DropzoneParentContainer" ".combo-editor-dropzone" "DropzoneParentContainer" ".combo-editor-dropzone"
)}} )}}

View File

@ -5,7 +5,7 @@
<div class="issue-navbar"> <div class="issue-navbar">
{{template "repo/issue/navbar" .}} {{template "repo/issue/navbar" .}}
{{if and (or .CanWriteIssues .CanWritePulls) .PageIsEditMilestone}} {{if and (or .CanWriteIssues .CanWritePulls) .PageIsEditMilestone}}
<div class="ui right floated secondary menu"> <div class="ui right">
<a class="ui primary button" href="{{$.RepoLink}}/milestones/new">{{ctx.Locale.Tr "repo.milestones.new"}}</a> <a class="ui primary button" href="{{$.RepoLink}}/milestones/new">{{ctx.Locale.Tr "repo.milestones.new"}}</a>
</div> </div>
{{end}} {{end}}
@ -36,9 +36,14 @@
</div> </div>
<div class="field"> <div class="field">
<label>{{ctx.Locale.Tr "repo.milestones.desc"}}</label> <label>{{ctx.Locale.Tr "repo.milestones.desc"}}</label>
<textarea name="content">{{.content}}</textarea> {{template "shared/combomarkdowneditor" (dict
"MarkdownPreviewInRepo" $.Repository
"MarkdownPreviewMode" "comment"
"TextareaName" "content"
"TextareaContent" .content
"TextareaPlaceholder" (ctx.Locale.Tr "repo.milestones.desc")
)}}
</div> </div>
<div class="divider"></div>
<div class="tw-text-right"> <div class="tw-text-right">
{{if .PageIsEditMilestone}} {{if .PageIsEditMilestone}}
<a class="ui primary basic button" href="{{.RepoLink}}/milestones"> <a class="ui primary basic button" href="{{.RepoLink}}/milestones">

View File

@ -142,8 +142,9 @@
<div class="ui form comment"> <div class="ui form comment">
<div class="field"> <div class="field">
{{template "shared/combomarkdowneditor" (dict {{template "shared/combomarkdowneditor" (dict
"MarkdownPreviewUrl" (print .Repository.Link "/markup") "CustomInit" true
"MarkdownPreviewContext" .RepoLink "MarkdownPreviewInRepo" $.Repository
"MarkdownPreviewMode" "comment"
"TextareaName" "content" "TextareaName" "content"
"DropzoneParentContainer" ".ui.form" "DropzoneParentContainer" ".ui.form"
)}} )}}

View File

@ -13,7 +13,7 @@
{{$canEditIssueTitle := and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}} {{$canEditIssueTitle := and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
<div class="issue-title" id="issue-title-display"> <div class="issue-title" id="issue-title-display">
<h1 class="tw-break-anywhere"> <h1 class="tw-break-anywhere">
{{ctx.RenderUtils.RenderIssueTitle .Issue.Title ($.Repository.ComposeMetas ctx) | RenderCodeBlock}} {{ctx.RenderUtils.RenderIssueTitle .Issue.Title ($.Repository.ComposeMetas ctx)}}
<span class="index">#{{.Issue.Index}}</span> <span class="index">#{{.Issue.Index}}</span>
</h1> </h1>
<div class="issue-title-buttons"> <div class="issue-title-buttons">

View File

@ -125,7 +125,7 @@
<span class="ui green label">{{ctx.Locale.Tr "repo.activity.published_release_label"}}</span> <span class="ui green label">{{ctx.Locale.Tr "repo.activity.published_release_label"}}</span>
{{.TagName}} {{.TagName}}
{{if not .IsTag}} {{if not .IsTag}}
<a class="title" href="{{$.RepoLink}}/src/{{.TagName | PathEscapeSegments}}">{{.Title | ctx.RenderUtils.RenderEmoji | RenderCodeBlock}}</a> <a class="title" href="{{$.RepoLink}}/src/{{.TagName | PathEscapeSegments}}">{{.Title | ctx.RenderUtils.RenderIssueSimpleTitle}}</a>
{{end}} {{end}}
{{DateUtils.TimeSince .CreatedUnix}} {{DateUtils.TimeSince .CreatedUnix}}
</p> </p>
@ -145,7 +145,7 @@
{{range .Activity.MergedPRs}} {{range .Activity.MergedPRs}}
<p class="desc"> <p class="desc">
<span class="ui purple label">{{ctx.Locale.Tr "repo.activity.merged_prs_label"}}</span> <span class="ui purple label">{{ctx.Locale.Tr "repo.activity.merged_prs_label"}}</span>
#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | ctx.RenderUtils.RenderEmoji | RenderCodeBlock}}</a> #{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | ctx.RenderUtils.RenderIssueSimpleTitle}}</a>
{{DateUtils.TimeSince .MergedUnix}} {{DateUtils.TimeSince .MergedUnix}}
</p> </p>
{{end}} {{end}}
@ -164,7 +164,7 @@
{{range .Activity.OpenedPRs}} {{range .Activity.OpenedPRs}}
<p class="desc"> <p class="desc">
<span class="ui green label">{{ctx.Locale.Tr "repo.activity.opened_prs_label"}}</span> <span class="ui green label">{{ctx.Locale.Tr "repo.activity.opened_prs_label"}}</span>
#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | ctx.RenderUtils.RenderEmoji | RenderCodeBlock}}</a> #{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | ctx.RenderUtils.RenderIssueSimpleTitle}}</a>
{{DateUtils.TimeSince .Issue.CreatedUnix}} {{DateUtils.TimeSince .Issue.CreatedUnix}}
</p> </p>
{{end}} {{end}}
@ -183,7 +183,7 @@
{{range .Activity.ClosedIssues}} {{range .Activity.ClosedIssues}}
<p class="desc"> <p class="desc">
<span class="ui red label">{{ctx.Locale.Tr "repo.activity.closed_issue_label"}}</span> <span class="ui red label">{{ctx.Locale.Tr "repo.activity.closed_issue_label"}}</span>
#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | ctx.RenderUtils.RenderEmoji | RenderCodeBlock}}</a> #{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | ctx.RenderUtils.RenderIssueSimpleTitle}}</a>
{{DateUtils.TimeSince .ClosedUnix}} {{DateUtils.TimeSince .ClosedUnix}}
</p> </p>
{{end}} {{end}}
@ -202,7 +202,7 @@
{{range .Activity.OpenedIssues}} {{range .Activity.OpenedIssues}}
<p class="desc"> <p class="desc">
<span class="ui green label">{{ctx.Locale.Tr "repo.activity.new_issue_label"}}</span> <span class="ui green label">{{ctx.Locale.Tr "repo.activity.new_issue_label"}}</span>
#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | ctx.RenderUtils.RenderEmoji | RenderCodeBlock}}</a> #{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | ctx.RenderUtils.RenderIssueSimpleTitle}}</a>
{{DateUtils.TimeSince .CreatedUnix}} {{DateUtils.TimeSince .CreatedUnix}}
</p> </p>
{{end}} {{end}}
@ -220,9 +220,9 @@
<span class="ui green label">{{ctx.Locale.Tr "repo.activity.unresolved_conv_label"}}</span> <span class="ui green label">{{ctx.Locale.Tr "repo.activity.unresolved_conv_label"}}</span>
#{{.Index}} #{{.Index}}
{{if .IsPull}} {{if .IsPull}}
<a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Title | ctx.RenderUtils.RenderEmoji | RenderCodeBlock}}</a> <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Title | ctx.RenderUtils.RenderIssueSimpleTitle}}</a>
{{else}} {{else}}
<a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | ctx.RenderUtils.RenderEmoji | RenderCodeBlock}}</a> <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | ctx.RenderUtils.RenderIssueSimpleTitle}}</a>
{{end}} {{end}}
{{DateUtils.TimeSince .UpdatedUnix}} {{DateUtils.TimeSince .UpdatedUnix}}
</p> </p>

View File

@ -50,12 +50,11 @@
</div> </div>
<div class="field"> <div class="field">
{{template "shared/combomarkdowneditor" (dict {{template "shared/combomarkdowneditor" (dict
"MarkdownPreviewUrl" (print .Repository.Link "/markup") "MarkdownPreviewInRepo" $.Repository
"MarkdownPreviewContext" .RepoLink "MarkdownPreviewMode" "comment"
"TextareaName" "content" "TextareaName" "content"
"TextareaContent" .content "TextareaContent" .content
"TextareaPlaceholder" (ctx.Locale.Tr "repo.release.message") "TextareaPlaceholder" (ctx.Locale.Tr "repo.release.message")
"TextareaAriaLabel" (ctx.Locale.Tr "repo.release.message")
"DropzoneParentContainer" "form" "DropzoneParentContainer" "form"
)}} )}}
</div> </div>

View File

@ -56,21 +56,21 @@
<button class="ui mini basic button escape-button">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button> <button class="ui mini basic button escape-button">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>
{{end}} {{end}}
</div> </div>
<a download href="{{$.RawFileLink}}"><span class="btn-octicon" data-tooltip-content="{{ctx.Locale.Tr "repo.download_file"}}">{{svg "octicon-download"}}</span></a> <a download class="btn-octicon" data-tooltip-content="{{ctx.Locale.Tr "repo.download_file"}}" href="{{$.RawFileLink}}">{{svg "octicon-download"}}</a>
<a id="copy-content" class="btn-octicon {{if not .CanCopyContent}} disabled{{end}}"{{if or .IsImageFile (and .HasSourceRenderedToggle (not .IsDisplayingSource))}} data-link="{{$.RawFileLink}}"{{end}} data-tooltip-content="{{if .CanCopyContent}}{{ctx.Locale.Tr "copy_content"}}{{else}}{{ctx.Locale.Tr "copy_type_unsupported"}}{{end}}">{{svg "octicon-copy" 14}}</a> <a id="copy-content" class="btn-octicon {{if not .CanCopyContent}} disabled{{end}}"{{if or .IsImageFile (and .HasSourceRenderedToggle (not .IsDisplayingSource))}} data-link="{{$.RawFileLink}}"{{end}} data-tooltip-content="{{if .CanCopyContent}}{{ctx.Locale.Tr "copy_content"}}{{else}}{{ctx.Locale.Tr "copy_type_unsupported"}}{{end}}">{{svg "octicon-copy"}}</a>
{{if .EnableFeed}} {{if .EnableFeed}}
<a class="btn-octicon" href="{{$.FeedURL}}/rss/{{$.BranchNameSubURL}}/{{PathEscapeSegments .TreePath}}" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}"> <a class="btn-octicon" href="{{$.FeedURL}}/rss/{{$.BranchNameSubURL}}/{{PathEscapeSegments .TreePath}}" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">
{{svg "octicon-rss" 14}} {{svg "octicon-rss"}}
</a> </a>
{{end}} {{end}}
{{if .Repository.CanEnableEditor}} {{if .Repository.CanEnableEditor}}
{{if .CanEditFile}} {{if .CanEditFile}}
<a href="{{.RepoLink}}/_edit/{{PathEscapeSegments .BranchName}}/{{PathEscapeSegments .TreePath}}"><span class="btn-octicon" data-tooltip-content="{{.EditFileTooltip}}">{{svg "octicon-pencil"}}</span></a> <a class="btn-octicon" data-tooltip-content="{{.EditFileTooltip}}" href="{{.RepoLink}}/_edit/{{PathEscapeSegments .BranchName}}/{{PathEscapeSegments .TreePath}}">{{svg "octicon-pencil"}}</a>
{{else}} {{else}}
<span class="btn-octicon disabled" data-tooltip-content="{{.EditFileTooltip}}">{{svg "octicon-pencil"}}</span> <span class="btn-octicon disabled" data-tooltip-content="{{.EditFileTooltip}}">{{svg "octicon-pencil"}}</span>
{{end}} {{end}}
{{if .CanDeleteFile}} {{if .CanDeleteFile}}
<a href="{{.RepoLink}}/_delete/{{PathEscapeSegments .BranchName}}/{{PathEscapeSegments .TreePath}}"><span class="btn-octicon btn-octicon-danger" data-tooltip-content="{{.DeleteFileTooltip}}">{{svg "octicon-trash"}}</span></a> <a class="btn-octicon btn-octicon-danger" data-tooltip-content="{{.DeleteFileTooltip}}" href="{{.RepoLink}}/_delete/{{PathEscapeSegments .BranchName}}/{{PathEscapeSegments .TreePath}}">{{svg "octicon-trash"}}</a>
{{else}} {{else}}
<span class="btn-octicon disabled" data-tooltip-content="{{.DeleteFileTooltip}}">{{svg "octicon-trash"}}</span> <span class="btn-octicon disabled" data-tooltip-content="{{.DeleteFileTooltip}}">{{svg "octicon-trash"}}</span>
{{end}} {{end}}

View File

@ -23,12 +23,12 @@
{{$content = ctx.Locale.Tr "repo.wiki.welcome"}} {{$content = ctx.Locale.Tr "repo.wiki.welcome"}}
{{end}} {{end}}
{{template "shared/combomarkdowneditor" (dict {{template "shared/combomarkdowneditor" (dict
"MarkdownPreviewUrl" (print .Repository.Link "/markup") "CustomInit" true
"MarkdownPreviewContext" .RepoLink "MarkdownPreviewInRepo" $.Repository
"MarkdownPreviewMode" "wiki"
"TextareaName" "content" "TextareaName" "content"
"TextareaPlaceholder" (ctx.Locale.Tr "repo.wiki.page_content")
"TextareaAriaLabel" (ctx.Locale.Tr "repo.wiki.page_content")
"TextareaContent" $content "TextareaContent" $content
"TextareaPlaceholder" (ctx.Locale.Tr "repo.wiki.page_content")
)}} )}}
<div class="field tw-mt-4"> <div class="field tw-mt-4">

View File

@ -1,23 +1,39 @@
{{/* {{/*
Template Attributes: Template Attributes:
* CustomInit: do not initialize the editor automatically
* ContainerId: id attribute for the container element * ContainerId: id attribute for the container element
* ContainerClasses: additional classes for the container element * ContainerClasses: additional classes for the container element
* MarkdownPreviewUrl: preview url for the preview tab * MarkdownPreviewInRepo: the repo to preview markdown
* MarkdownPreviewContext: preview context for the preview tab * MarkdownPreviewContext: preview context (the related url path when rendering) for the preview tab, eg: repo link or user home link
* MarkdownPreviewMode: content mode for the editor, eg: wiki, comment or default
* TextareaName: name attribute for the textarea * TextareaName: name attribute for the textarea
* TextareaContent: content for the textarea * TextareaContent: content for the textarea
* TextareaMaxLength: maxlength attribute for the textarea
* TextareaPlaceholder: placeholder attribute for the textarea * TextareaPlaceholder: placeholder attribute for the textarea
* TextareaAriaLabel: aria-label attribute for the textarea * TextareaAriaLabel: aria-label attribute for the textarea
* DropzoneParentContainer: container for file upload (leave it empty if no upload) * DropzoneParentContainer: container for file upload (leave it empty if no upload)
* DisableAutosize: whether to disable automatic height resizing * DisableAutosize: whether to disable automatic height resizing
*/}} */}}
<div {{if .ContainerId}}id="{{.ContainerId}}"{{end}} class="combo-markdown-editor {{.ContainerClasses}}" data-dropzone-parent-container="{{.DropzoneParentContainer}}"> {{$ariaLabel := or .TextareaAriaLabel .TextareaPlaceholder}}
{{if .MarkdownPreviewUrl}} {{$repo := .MarkdownPreviewInRepo}}
{{$previewContext := .MarkdownPreviewContext}}
{{$previewMode := .MarkdownPreviewMode}}
{{$previewUrl := print AppSubUrl "/-/markup"}}
{{if $repo}}
{{$previewUrl = print $repo.Link "/markup"}}
{{end}}
{{$supportEasyMDE := or (eq $previewMode "comment") (eq $previewMode "wiki")}}
<div {{if .ContainerId}}id="{{.ContainerId}}"{{end}} class="combo-markdown-editor {{if .CustomInit}}custom-init{{end}} {{.ContainerClasses}}"
data-dropzone-parent-container="{{.DropzoneParentContainer}}"
data-content-mode="{{$previewMode}}"
data-support-easy-mde="{{$supportEasyMDE}}"
data-preview-url="{{$previewUrl}}"
data-preview-context="{{$previewContext}}"
>
<div class="ui top tabular menu"> <div class="ui top tabular menu">
<a class="active item" data-tab-for="markdown-writer">{{template "shared/misc/tabtitle" (ctx.Locale.Tr "write")}}</a> <a class="active item" data-tab-for="markdown-writer">{{template "shared/misc/tabtitle" (ctx.Locale.Tr "write")}}</a>
<a class="item" data-tab-for="markdown-previewer" data-preview-url="{{.MarkdownPreviewUrl}}" data-preview-context="{{.MarkdownPreviewContext}}">{{template "shared/misc/tabtitle" (ctx.Locale.Tr "preview")}}</a> <a class="item" data-tab-for="markdown-previewer">{{template "shared/misc/tabtitle" (ctx.Locale.Tr "preview")}}</a>
</div> </div>
{{end}}
<div class="ui tab active" data-tab-panel="markdown-writer"> <div class="ui tab active" data-tab-panel="markdown-writer">
<markdown-toolbar> <markdown-toolbar>
<div class="markdown-toolbar-group"> <div class="markdown-toolbar-group">
@ -40,17 +56,25 @@ Template Attributes:
<md-task-list class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.list.task.tooltip"}}">{{svg "octicon-tasklist"}}</md-task-list> <md-task-list class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.list.task.tooltip"}}">{{svg "octicon-tasklist"}}</md-task-list>
<button class="markdown-toolbar-button markdown-button-table-add" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.table.add.tooltip"}}">{{svg "octicon-table"}}</button> <button class="markdown-toolbar-button markdown-button-table-add" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.table.add.tooltip"}}">{{svg "octicon-table"}}</button>
</div> </div>
{{if eq $previewMode "comment"}}
<div class="markdown-toolbar-group"> <div class="markdown-toolbar-group">
<md-mention class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.mention.tooltip"}}">{{svg "octicon-mention"}}</md-mention> <md-mention class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.mention.tooltip"}}">{{svg "octicon-mention"}}</md-mention>
<md-ref class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.ref.tooltip"}}">{{svg "octicon-cross-reference"}}</md-ref> <md-ref class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.ref.tooltip"}}">{{svg "octicon-cross-reference"}}</md-ref>
</div> </div>
{{end}}
<div class="markdown-toolbar-group"> <div class="markdown-toolbar-group">
<button class="markdown-toolbar-button markdown-switch-monospace" role="switch" data-enable-text="{{ctx.Locale.Tr "editor.buttons.enable_monospace_font"}}" data-disable-text="{{ctx.Locale.Tr "editor.buttons.disable_monospace_font"}}">{{svg "octicon-typography"}}</button> <button class="markdown-toolbar-button markdown-switch-monospace" role="switch" data-enable-text="{{ctx.Locale.Tr "editor.buttons.enable_monospace_font"}}" data-disable-text="{{ctx.Locale.Tr "editor.buttons.disable_monospace_font"}}">{{svg "octicon-typography"}}</button>
{{if $supportEasyMDE}}
<button class="markdown-toolbar-button markdown-switch-easymde" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.switch_to_legacy.tooltip"}}">{{svg "octicon-arrow-switch"}}</button> <button class="markdown-toolbar-button markdown-switch-easymde" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.switch_to_legacy.tooltip"}}">{{svg "octicon-arrow-switch"}}</button>
{{end}}
</div> </div>
</markdown-toolbar> </markdown-toolbar>
<text-expander keys=": @ #" multiword="#" suffix=""> <text-expander keys=": @ #" multiword="#" suffix="">
<textarea class="markdown-text-editor"{{if .TextareaName}} name="{{.TextareaName}}"{{end}}{{if .TextareaPlaceholder}} placeholder="{{.TextareaPlaceholder}}"{{end}}{{if .TextareaAriaLabel}} aria-label="{{.TextareaAriaLabel}}"{{end}}{{if .DisableAutosize}} data-disable-autosize="{{.DisableAutosize}}"{{end}}>{{.TextareaContent}}</textarea> <textarea class="markdown-text-editor"
{{if .TextareaName}}name="{{.TextareaName}}"{{end}} {{if .TextareaMaxLength}}maxlength="{{.TextareaMaxLength}}"{{end}}
{{if .TextareaPlaceholder}}placeholder="{{.TextareaPlaceholder}}"{{end}} {{if $ariaLabel}}aria-label="{{$ariaLabel}}"{{end}}
{{if .DisableAutosize}}data-disable-autosize="{{.DisableAutosize}}"{{end}}
>{{.TextareaContent}}</textarea>
</text-expander> </text-expander>
<script> <script>
if (localStorage?.getItem('markdown-editor-monospace') === 'true') { if (localStorage?.getItem('markdown-editor-monospace') === 'true') {

View File

@ -13,7 +13,7 @@
<div class="flex-item-main"> <div class="flex-item-main">
<div class="flex-item-header"> <div class="flex-item-header">
<div class="flex-item-title"> <div class="flex-item-title">
<a class="tw-no-underline issue-title" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}">{{ctx.RenderUtils.RenderEmoji .Title | RenderCodeBlock}}</a> <a class="tw-no-underline issue-title" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}">{{.Title | ctx.RenderUtils.RenderIssueSimpleTitle}}</a>
{{if .IsPull}} {{if .IsPull}}
{{if (index $.CommitStatuses .PullRequest.ID)}} {{if (index $.CommitStatuses .PullRequest.ID)}}
{{template "repo/commit_statuses" dict "Status" (index $.CommitLastStatus .PullRequest.ID) "Statuses" (index $.CommitStatuses .PullRequest.ID)}} {{template "repo/commit_statuses" dict "Status" (index $.CommitLastStatus .PullRequest.ID) "Statuses" (index $.CommitStatuses .PullRequest.ID)}}

View File

@ -48,10 +48,11 @@
</div> </div>
</form> </form>
{{end}}{{/*if .EnablePasswordSignInForm*/}} {{end}}{{/*if .EnablePasswordSignInForm*/}}
{{if and .OAuth2Providers .EnableOpenIDSignIn .EnablePasswordSignInForm}} {{$showOAuth2Methods := or .OAuth2Providers .EnableOpenIDSignIn}}
{{if and $showOAuth2Methods .EnablePasswordSignInForm}}
<div class="divider divider-text">{{ctx.Locale.Tr "sign_in_or"}}</div> <div class="divider divider-text">{{ctx.Locale.Tr "sign_in_or"}}</div>
{{end}} {{end}}
{{if and .OAuth2Providers .EnableOpenIDSignIn}} {{if $showOAuth2Methods}}
{{template "user/auth/oauth_container" .}} {{template "user/auth/oauth_container" .}}
{{end}} {{end}}
</div> </div>

View File

@ -47,8 +47,8 @@
</button> </button>
</div> </div>
{{end}} {{end}}
{{$showOAuth2Methods := or .OAuth2Providers .EnableOpenIDSignIn}}
{{if and .OAuth2Providers .EnableOpenIDSignIn}} {{if $showOAuth2Methods}}
<div class="divider divider-text">{{ctx.Locale.Tr "sign_in_or"}}</div> <div class="divider divider-text">{{ctx.Locale.Tr "sign_in_or"}}</div>
{{template "user/auth/oauth_container" .}} {{template "user/auth/oauth_container" .}}
{{end}} {{end}}

View File

@ -100,11 +100,11 @@
<a href="{{AppSubUrl}}/{{$push.CompareURL}}">{{ctx.Locale.Tr "action.compare_commits" $push.Len}} »</a> <a href="{{AppSubUrl}}/{{$push.CompareURL}}">{{ctx.Locale.Tr "action.compare_commits" $push.Len}} »</a>
{{end}} {{end}}
{{else if .GetOpType.InActions "create_issue"}} {{else if .GetOpType.InActions "create_issue"}}
<span class="text truncate issue title">{{index .GetIssueInfos 1 | ctx.RenderUtils.RenderEmoji | RenderCodeBlock}}</span> <span class="text truncate issue title">{{index .GetIssueInfos 1 | ctx.RenderUtils.RenderIssueSimpleTitle}}</span>
{{else if .GetOpType.InActions "create_pull_request"}} {{else if .GetOpType.InActions "create_pull_request"}}
<span class="text truncate issue title">{{index .GetIssueInfos 1 | ctx.RenderUtils.RenderEmoji | RenderCodeBlock}}</span> <span class="text truncate issue title">{{index .GetIssueInfos 1 | ctx.RenderUtils.RenderIssueSimpleTitle}}</span>
{{else if .GetOpType.InActions "comment_issue" "approve_pull_request" "reject_pull_request" "comment_pull"}} {{else if .GetOpType.InActions "comment_issue" "approve_pull_request" "reject_pull_request" "comment_pull"}}
<a href="{{.GetCommentLink ctx}}" class="text truncate issue title">{{(.GetIssueTitle ctx) | ctx.RenderUtils.RenderEmoji | RenderCodeBlock}}</a> <a href="{{.GetCommentLink ctx}}" class="text truncate issue title">{{(.GetIssueTitle ctx) | ctx.RenderUtils.RenderIssueSimpleTitle}}</a>
{{$comment := index .GetIssueInfos 1}} {{$comment := index .GetIssueInfos 1}}
{{if $comment}} {{if $comment}}
<div class="markup tw-text-14">{{ctx.RenderUtils.MarkdownToHtml $comment}}</div> <div class="markup tw-text-14">{{ctx.RenderUtils.MarkdownToHtml $comment}}</div>
@ -112,7 +112,7 @@
{{else if .GetOpType.InActions "merge_pull_request"}} {{else if .GetOpType.InActions "merge_pull_request"}}
<div class="flex-item-body text black">{{index .GetIssueInfos 1}}</div> <div class="flex-item-body text black">{{index .GetIssueInfos 1}}</div>
{{else if .GetOpType.InActions "close_issue" "reopen_issue" "close_pull_request" "reopen_pull_request"}} {{else if .GetOpType.InActions "close_issue" "reopen_issue" "close_pull_request" "reopen_pull_request"}}
<span class="text truncate issue title">{{(.GetIssueTitle ctx) | ctx.RenderUtils.RenderEmoji | RenderCodeBlock}}</span> <span class="text truncate issue title">{{(.GetIssueTitle ctx) | ctx.RenderUtils.RenderIssueSimpleTitle}}</span>
{{else if .GetOpType.InActions "pull_review_dismissed"}} {{else if .GetOpType.InActions "pull_review_dismissed"}}
<div class="flex-item-body text black">{{ctx.Locale.Tr "action.review_dismissed_reason"}}</div> <div class="flex-item-body text black">{{ctx.Locale.Tr "action.review_dismissed_reason"}}</div>
<div class="flex-item-body text black">{{index .GetIssueInfos 2 | ctx.RenderUtils.RenderEmoji}}</div> <div class="flex-item-body text black">{{index .GetIssueInfos 2 | ctx.RenderUtils.RenderEmoji}}</div>

View File

@ -53,7 +53,7 @@
<div class="notifications-bottom-row tw-text-16 tw-py-0.5"> <div class="notifications-bottom-row tw-text-16 tw-py-0.5">
<span class="issue-title tw-break-anywhere"> <span class="issue-title tw-break-anywhere">
{{if .Issue}} {{if .Issue}}
{{.Issue.Title | ctx.RenderUtils.RenderEmoji | RenderCodeBlock}} {{.Issue.Title | ctx.RenderUtils.RenderIssueSimpleTitle}}
{{else}} {{else}}
{{.Repository.FullName}} {{.Repository.FullName}}
{{end}} {{end}}

View File

@ -40,7 +40,7 @@
{{ctx.Locale.Tr "settings.manage_emails"}} {{ctx.Locale.Tr "settings.manage_emails"}}
</h4> </h4>
<div class="ui attached segment"> <div class="ui attached segment">
<div class="ui list"> <div class="ui list flex-items-block">
{{if $.EnableNotifyMail}} {{if $.EnableNotifyMail}}
<div class="item"> <div class="item">
<div class="tw-mb-2">{{ctx.Locale.Tr "settings.email_desc"}}</div> <div class="tw-mb-2">{{ctx.Locale.Tr "settings.email_desc"}}</div>
@ -65,27 +65,34 @@
</div> </div>
{{end}} {{end}}
{{if not ($.UserDisabledFeatures.Contains "manage_credentials")}} {{if not ($.UserDisabledFeatures.Contains "manage_credentials")}}
{{range .Emails}} {{range .Emails}}
<div class="item"> <div class="item tw-flex-wrap">
{{if not .IsPrimary}} <div class="content tw-flex-1">
<div class="right floated content"> <strong>{{.Email}}</strong>
{{if .IsPrimary}}
<div class="ui primary label">{{ctx.Locale.Tr "settings.primary"}}</div>
{{end}}
{{if .IsActivated}}
<div class="ui green label">{{ctx.Locale.Tr "settings.activated"}}</div>
{{else}}
<div class="ui label">{{ctx.Locale.Tr "settings.requires_activation"}}</div>
{{end}}
</div>
<div class="flex-text-block">
{{if not .IsPrimary}}
<button class="ui red tiny button delete-button" data-modal-id="delete-email" data-url="{{AppSubUrl}}/user/settings/account/email/delete" data-id="{{.ID}}"> <button class="ui red tiny button delete-button" data-modal-id="delete-email" data-url="{{AppSubUrl}}/user/settings/account/email/delete" data-id="{{.ID}}">
{{ctx.Locale.Tr "settings.delete_email"}} {{ctx.Locale.Tr "settings.delete_email"}}
</button> </button>
</div> {{if .CanBePrimary}}
{{if .CanBePrimary}}
<div class="right floated content">
<form action="{{AppSubUrl}}/user/settings/account/email" method="post"> <form action="{{AppSubUrl}}/user/settings/account/email" method="post">
{{$.CsrfTokenHtml}} {{$.CsrfTokenHtml}}
<input name="_method" type="hidden" value="PRIMARY"> <input name="_method" type="hidden" value="PRIMARY">
<input name="id" type="hidden" value="{{.ID}}"> <input name="id" type="hidden" value="{{.ID}}">
<button class="ui primary tiny button">{{ctx.Locale.Tr "settings.primary_email"}}</button> <button class="ui primary tiny button">{{ctx.Locale.Tr "settings.primary_email"}}</button>
</form> </form>
</div> {{end}}
{{end}} {{end}}
{{end}} {{if not .IsActivated}}
{{if not .IsActivated}}
<div class="right floated content">
<form action="{{AppSubUrl}}/user/settings/account/email" method="post"> <form action="{{AppSubUrl}}/user/settings/account/email" method="post">
{{$.CsrfTokenHtml}} {{$.CsrfTokenHtml}}
<input name="_method" type="hidden" value="SENDACTIVATION"> <input name="_method" type="hidden" value="SENDACTIVATION">
@ -96,22 +103,11 @@
<button class="ui primary tiny button">{{ctx.Locale.Tr "settings.activate_email"}}</button> <button class="ui primary tiny button">{{ctx.Locale.Tr "settings.activate_email"}}</button>
{{end}} {{end}}
</form> </form>
{{end}}
</div> </div>
{{end}}
<div class="content tw-py-2">
<strong>{{.Email}}</strong>
{{if .IsPrimary}}
<div class="ui primary label">{{ctx.Locale.Tr "settings.primary"}}</div>
{{end}}
{{if .IsActivated}}
<div class="ui green label">{{ctx.Locale.Tr "settings.activated"}}</div>
{{else}}
<div class="ui label">{{ctx.Locale.Tr "settings.requires_activation"}}</div>
{{end}}
</div> </div>
</div> {{end}}{{/* range Emails */}}
{{end}} {{end}}{{/* if manage_credentials */}}
{{end}}
</div> </div>
</div> </div>
{{end}} {{end}}

View File

@ -29,6 +29,7 @@
<p id="signed-user-email">{{.SignedUser.Email}}</p> <p id="signed-user-email">{{.SignedUser.Email}}</p>
</div> </div>
<div class="field {{if .Err_Description}}error{{end}}"> <div class="field {{if .Err_Description}}error{{end}}">
{{/* it is rendered as markdown, but the length is limited, so at the moment we do not use the markdown editor here */}}
<label for="description">{{ctx.Locale.Tr "user.user_bio"}}</label> <label for="description">{{ctx.Locale.Tr "user.user_bio"}}</label>
<textarea id="description" name="description" rows="2" placeholder="{{ctx.Locale.Tr "settings.biography_placeholder"}}" maxlength="255">{{.SignedUser.Description}}</textarea> <textarea id="description" name="description" rows="2" placeholder="{{ctx.Locale.Tr "settings.biography_placeholder"}}" maxlength="255">{{.SignedUser.Description}}</textarea>
</div> </div>

View File

@ -42,6 +42,24 @@ func TestPackageSwift(t *testing.T) {
url := fmt.Sprintf("/api/packages/%s/swift", user.Name) url := fmt.Sprintf("/api/packages/%s/swift", user.Name)
t.Run("CheckLogin", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithBody(t, "POST", url, strings.NewReader(""))
MakeRequest(t, req, http.StatusUnauthorized)
req = NewRequestWithBody(t, "POST", url, strings.NewReader("")).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusOK)
req = NewRequestWithBody(t, "POST", url+"/login", strings.NewReader(""))
MakeRequest(t, req, http.StatusUnauthorized)
req = NewRequestWithBody(t, "POST", url+"/login", strings.NewReader("")).
AddBasicAuth(user.Name)
MakeRequest(t, req, http.StatusOK)
})
t.Run("CheckAcceptMediaType", func(t *testing.T) { t.Run("CheckAcceptMediaType", func(t *testing.T) {
defer tests.PrintCurrentTest(t)() defer tests.PrintCurrentTest(t)()

View File

@ -554,6 +554,10 @@ func TestPullRetargetChildOnBranchDelete(t *testing.T) {
testPullMerge(t, session, elemBasePR[1], elemBasePR[2], elemBasePR[4], repo_model.MergeStyleMerge, true) testPullMerge(t, session, elemBasePR[1], elemBasePR[2], elemBasePR[4], repo_model.MergeStyleMerge, true)
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user2", Name: "repo1"})
branchBasePR := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "base-pr"})
assert.True(t, branchBasePR.IsDeleted)
// Check child PR // Check child PR
req := NewRequest(t, "GET", test.RedirectURL(respChildPR)) req := NewRequest(t, "GET", test.RedirectURL(respChildPR))
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
@ -584,6 +588,10 @@ func TestPullDontRetargetChildOnWrongRepo(t *testing.T) {
testPullMerge(t, session, elemBasePR[1], elemBasePR[2], elemBasePR[4], repo_model.MergeStyleMerge, true) testPullMerge(t, session, elemBasePR[1], elemBasePR[2], elemBasePR[4], repo_model.MergeStyleMerge, true)
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"})
branchBasePR := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "base-pr"})
assert.True(t, branchBasePR.IsDeleted)
// Check child PR // Check child PR
req := NewRequest(t, "GET", test.RedirectURL(respChildPR)) req := NewRequest(t, "GET", test.RedirectURL(respChildPR))
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
@ -598,6 +606,27 @@ func TestPullDontRetargetChildOnWrongRepo(t *testing.T) {
}) })
} }
func TestPullRequestMergedWithNoPermissionDeleteBranch(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
session := loginUser(t, "user4")
testRepoFork(t, session, "user2", "repo1", "user4", "repo1", "")
testEditFileToNewBranch(t, session, "user4", "repo1", "master", "base-pr", "README.md", "Hello, World\n(Edited - TestPullDontRetargetChildOnWrongRepo - base PR)\n")
respBasePR := testPullCreate(t, session, "user4", "repo1", false, "master", "base-pr", "Base Pull Request")
elemBasePR := strings.Split(test.RedirectURL(respBasePR), "/")
assert.EqualValues(t, "pulls", elemBasePR[3])
// user2 has no permission to delete branch of repo user1/repo1
session2 := loginUser(t, "user2")
testPullMerge(t, session2, elemBasePR[1], elemBasePR[2], elemBasePR[4], repo_model.MergeStyleMerge, true)
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user4", Name: "repo1"})
branchBasePR := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "base-pr"})
// branch has not been deleted
assert.False(t, branchBasePR.IsDeleted)
})
}
func TestPullMergeIndexerNotifier(t *testing.T) { func TestPullMergeIndexerNotifier(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
// create a pull request // create a pull request

View File

@ -96,6 +96,11 @@
font-size: 0.85em; font-size: 0.85em;
} }
.combo-markdown-editor .ui.tab.markup[data-tab-panel="markdown-previewer"] {
border-bottom: 1px solid var(--color-secondary);
padding-bottom: 1rem;
}
text-expander { text-expander {
display: block; display: block;
position: relative; position: relative;

View File

@ -126,12 +126,6 @@
cursor: pointer; cursor: pointer;
} }
.ui.list .list > .item [class*="right floated"],
.ui.list > .item [class*="right floated"] {
float: right;
margin: 0 0 0 1em;
}
.ui.menu .ui.list > .item, .ui.menu .ui.list > .item,
.ui.menu .ui.list .list > .item { .ui.menu .ui.list .list > .item {
display: list-item; display: list-item;

View File

@ -633,18 +633,6 @@
} }
} }
.ui.floated.menu {
float: left;
margin: 0 0.5rem 0 0;
}
.ui.floated.menu .item:last-child::before {
display: none;
}
.ui.right.floated.menu {
float: right;
margin: 0 0 0 0.5rem;
}
.ui.borderless.menu .item::before, .ui.borderless.menu .item::before,
.ui.borderless.menu .item .menu .item::before, .ui.borderless.menu .item .menu .item::before,
.ui.menu .borderless.item::before { .ui.menu .borderless.item::before {

View File

@ -1005,7 +1005,7 @@ td .commit-summary {
} }
.repository.view.issue .comment-list .code-comment .comment-content { .repository.view.issue .comment-list .code-comment .comment-content {
margin-left: 36px; margin-left: 24px;
} }
.repository.view.issue .comment-list .comment > .avatar { .repository.view.issue .comment-list .comment > .avatar {

View File

@ -102,19 +102,11 @@
cursor: pointer; cursor: pointer;
} }
.comment-code-cloud .ui.active.tab {
padding: 0.5em;
}
.comment-code-cloud .ui.active.tab.markup { .comment-code-cloud .ui.active.tab.markup {
padding: 1em; padding: 1em;
min-height: 168px; min-height: 168px;
} }
.comment-code-cloud .ui.tabular.menu {
margin: 0.5em;
}
.comment-code-cloud .editor-statusbar { .comment-code-cloud .editor-statusbar {
display: none; display: none;
} }
@ -123,23 +115,6 @@
padding: 10px 0; padding: 10px 0;
} }
.comment-code-cloud .footer .markup-info {
display: inline-block;
margin: 5px 0;
font-size: 12px;
color: var(--color-text-light);
}
.comment-code-cloud .footer .ui.right.floated {
padding-top: 6px;
}
.comment-code-cloud .footer::after {
clear: both;
content: "";
display: block;
}
.diff-file-body .comment-form { .diff-file-body .comment-form {
margin: 0 0 0 3em; margin: 0 0 0 3em;
} }

View File

@ -86,6 +86,8 @@ const sfc = {
textCreateBranch: elRoot.getAttribute('data-text-create-branch'), textCreateBranch: elRoot.getAttribute('data-text-create-branch'),
textCreateRefFrom: elRoot.getAttribute('data-text-create-ref-from'), textCreateRefFrom: elRoot.getAttribute('data-text-create-ref-from'),
textNoResults: elRoot.getAttribute('data-text-no-results'), textNoResults: elRoot.getAttribute('data-text-no-results'),
textViewAllBranches: elRoot.getAttribute('data-text-view-all-branches'),
textViewAllTags: elRoot.getAttribute('data-text-view-all-tags'),
currentRepoDefaultBranch: elRoot.getAttribute('data-current-repo-default-branch'), currentRepoDefaultBranch: elRoot.getAttribute('data-current-repo-default-branch'),
currentRepoLink: elRoot.getAttribute('data-current-repo-link'), currentRepoLink: elRoot.getAttribute('data-current-repo-link'),
@ -99,6 +101,7 @@ const sfc = {
showTabBranches: shouldShowTabBranches, showTabBranches: shouldShowTabBranches,
showTabTags: elRoot.getAttribute('data-show-tab-tags') === 'true', showTabTags: elRoot.getAttribute('data-show-tab-tags') === 'true',
allowCreateNewRef: elRoot.getAttribute('data-allow-create-new-ref') === 'true', allowCreateNewRef: elRoot.getAttribute('data-allow-create-new-ref') === 'true',
showViewAllRefsEntry: elRoot.getAttribute('data-show-view-all-refs-entry') === 'true',
enableFeed: elRoot.getAttribute('data-enable-feed') === 'true', enableFeed: elRoot.getAttribute('data-enable-feed') === 'true',
}; };
@ -281,6 +284,11 @@ export default sfc; // activate IDE's Vue plugin
<div class="message" v-if="showNoResults"> <div class="message" v-if="showNoResults">
{{ textNoResults }} {{ textNoResults }}
</div> </div>
<template v-if="showViewAllRefsEntry">
<div class="divider tw-m-0"/>
<a v-if="selectedTab === 'branches'" class="item" :href="currentRepoLink + '/branches'">{{ textViewAllBranches }}</a>
<a v-if="selectedTab === 'tags'" class="item" :href="currentRepoLink + '/tags'">{{ textViewAllTags }}</a>
</template>
</div> </div>
</div> </div>
</template> </template>

View File

@ -134,19 +134,17 @@ function getFileBasedOptions(filename: string, lineWrapExts: string[]) {
} }
function togglePreviewDisplay(previewable: boolean) { function togglePreviewDisplay(previewable: boolean) {
const previewTab = document.querySelector('a[data-tab="preview"]'); const previewTab = document.querySelector<HTMLElement>('a[data-tab="preview"]');
if (!previewTab) return; if (!previewTab) return;
if (previewable) { if (previewable) {
const newUrl = (previewTab.getAttribute('data-url') || '').replace(/(.*)\/.*/, `$1/markup`);
previewTab.setAttribute('data-url', newUrl);
previewTab.style.display = ''; previewTab.style.display = '';
} else { } else {
previewTab.style.display = 'none'; previewTab.style.display = 'none';
// If the "preview" tab was active, user changes the filename to a non-previewable one, // If the "preview" tab was active, user changes the filename to a non-previewable one,
// then the "preview" tab becomes inactive (hidden), so the "write" tab should become active // then the "preview" tab becomes inactive (hidden), so the "write" tab should become active
if (previewTab.classList.contains('active')) { if (previewTab.classList.contains('active')) {
const writeTab = document.querySelector('a[data-tab="write"]'); const writeTab = document.querySelector<HTMLElement>('a[data-tab="write"]');
writeTab.click(); writeTab.click();
} }
} }

View File

@ -1,5 +1,7 @@
import {applyAreYouSure, initAreYouSure} from '../vendor/jquery.are-you-sure.ts'; import {applyAreYouSure, initAreYouSure} from '../vendor/jquery.are-you-sure.ts';
import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.ts'; import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.ts';
import {queryElems} from '../utils/dom.ts';
import {initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
export function initGlobalFormDirtyLeaveConfirm() { export function initGlobalFormDirtyLeaveConfirm() {
initAreYouSure(window.jQuery); initAreYouSure(window.jQuery);
@ -11,7 +13,7 @@ export function initGlobalFormDirtyLeaveConfirm() {
} }
export function initGlobalEnterQuickSubmit() { export function initGlobalEnterQuickSubmit() {
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e: KeyboardEvent & {target: HTMLElement}) => {
if (e.key !== 'Enter') return; if (e.key !== 'Enter') return;
const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey); const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey);
if (hasCtrlOrMeta && e.target.matches('textarea')) { if (hasCtrlOrMeta && e.target.matches('textarea')) {
@ -27,3 +29,7 @@ export function initGlobalEnterQuickSubmit() {
} }
}); });
} }
export function initGlobalComboMarkdownEditor() {
queryElems<HTMLElement>(document, '.combo-markdown-editor:not(.custom-init)', (el) => initComboMarkdownEditor(el));
}

View File

@ -1,6 +1,5 @@
import '@github/markdown-toolbar-element'; import '@github/markdown-toolbar-element';
import '@github/text-expander-element'; import '@github/text-expander-element';
import $ from 'jquery';
import {attachTribute} from '../tribute.ts'; import {attachTribute} from '../tribute.ts';
import {hideElem, showElem, autosize, isElemVisible} from '../../utils/dom.ts'; import {hideElem, showElem, autosize, isElemVisible} from '../../utils/dom.ts';
import { import {
@ -23,6 +22,8 @@ import {
} from './EditorMarkdown.ts'; } from './EditorMarkdown.ts';
import {DropzoneCustomEventReloadFiles, initDropzone} from '../dropzone.ts'; import {DropzoneCustomEventReloadFiles, initDropzone} from '../dropzone.ts';
import {createTippy} from '../../modules/tippy.ts'; import {createTippy} from '../../modules/tippy.ts';
import {fomanticQuery} from '../../modules/fomantic/base.ts';
import type EasyMDE from 'easymde';
let elementIdCounter = 0; let elementIdCounter = 0;
@ -48,18 +49,23 @@ export function validateTextareaNonEmpty(textarea) {
return true; return true;
} }
type ComboMarkdownEditorOptions = {
editorHeights?: {minHeight?: string, height?: string, maxHeight?: string},
easyMDEOptions?: EasyMDE.Options,
};
export class ComboMarkdownEditor { export class ComboMarkdownEditor {
static EventEditorContentChanged = EventEditorContentChanged; static EventEditorContentChanged = EventEditorContentChanged;
static EventUploadStateChanged = EventUploadStateChanged; static EventUploadStateChanged = EventUploadStateChanged;
public container : HTMLElement; public container : HTMLElement;
// TODO: use correct types to replace these "any" types options: ComboMarkdownEditorOptions;
options: any;
tabEditor: HTMLElement; tabEditor: HTMLElement;
tabPreviewer: HTMLElement; tabPreviewer: HTMLElement;
supportEasyMDE: boolean;
easyMDE: any; easyMDE: any;
easyMDEToolbarActions: any; easyMDEToolbarActions: any;
easyMDEToolbarDefault: any; easyMDEToolbarDefault: any;
@ -71,11 +77,12 @@ export class ComboMarkdownEditor {
dropzone: HTMLElement; dropzone: HTMLElement;
attachedDropzoneInst: any; attachedDropzoneInst: any;
previewMode: string;
previewUrl: string; previewUrl: string;
previewContext: string; previewContext: string;
previewMode: string;
constructor(container, options = {}) { constructor(container, options:ComboMarkdownEditorOptions = {}) {
if (container._giteaComboMarkdownEditor) throw new Error('ComboMarkdownEditor already initialized');
container._giteaComboMarkdownEditor = this; container._giteaComboMarkdownEditor = this;
this.options = options; this.options = options;
this.container = container; this.container = container;
@ -99,6 +106,10 @@ export class ComboMarkdownEditor {
} }
setupContainer() { setupContainer() {
this.supportEasyMDE = this.container.getAttribute('data-support-easy-mde') === 'true';
this.previewMode = this.container.getAttribute('data-content-mode');
this.previewUrl = this.container.getAttribute('data-preview-url');
this.previewContext = this.container.getAttribute('data-preview-context');
initTextExpander(this.container.querySelector('text-expander')); initTextExpander(this.container.querySelector('text-expander'));
} }
@ -137,12 +148,14 @@ export class ComboMarkdownEditor {
monospaceButton.setAttribute('aria-checked', String(enabled)); monospaceButton.setAttribute('aria-checked', String(enabled));
}); });
const easymdeButton = this.container.querySelector('.markdown-switch-easymde'); if (this.supportEasyMDE) {
easymdeButton.addEventListener('click', async (e) => { const easymdeButton = this.container.querySelector('.markdown-switch-easymde');
e.preventDefault(); easymdeButton.addEventListener('click', async (e) => {
this.userPreferredEditor = 'easymde'; e.preventDefault();
await this.switchToEasyMDE(); this.userPreferredEditor = 'easymde';
}); await this.switchToEasyMDE();
});
}
this.initMarkdownButtonTableAdd(); this.initMarkdownButtonTableAdd();
@ -187,6 +200,7 @@ export class ComboMarkdownEditor {
setupTab() { setupTab() {
const tabs = this.container.querySelectorAll<HTMLElement>('.tabular.menu > .item'); const tabs = this.container.querySelectorAll<HTMLElement>('.tabular.menu > .item');
if (!tabs.length) return;
// Fomantic Tab requires the "data-tab" to be globally unique. // Fomantic Tab requires the "data-tab" to be globally unique.
// So here it uses our defined "data-tab-for" and "data-tab-panel" to generate the "data-tab" attribute for Fomantic. // So here it uses our defined "data-tab-for" and "data-tab-panel" to generate the "data-tab" attribute for Fomantic.
@ -207,11 +221,8 @@ export class ComboMarkdownEditor {
}); });
}); });
$(tabs).tab(); fomanticQuery(tabs).tab();
this.previewUrl = this.tabPreviewer.getAttribute('data-preview-url');
this.previewContext = this.tabPreviewer.getAttribute('data-preview-context');
this.previewMode = this.options.previewMode ?? 'comment';
this.tabPreviewer.addEventListener('click', async () => { this.tabPreviewer.addEventListener('click', async () => {
const formData = new FormData(); const formData = new FormData();
formData.append('mode', this.previewMode); formData.append('mode', this.previewMode);
@ -219,7 +230,7 @@ export class ComboMarkdownEditor {
formData.append('text', this.value()); formData.append('text', this.value());
const response = await POST(this.previewUrl, {data: formData}); const response = await POST(this.previewUrl, {data: formData});
const data = await response.text(); const data = await response.text();
renderPreviewPanelContent($(panelPreviewer), data); renderPreviewPanelContent(panelPreviewer, data);
}); });
} }
@ -284,7 +295,7 @@ export class ComboMarkdownEditor {
} }
async switchToUserPreference() { async switchToUserPreference() {
if (this.userPreferredEditor === 'easymde') { if (this.userPreferredEditor === 'easymde' && this.supportEasyMDE) {
await this.switchToEasyMDE(); await this.switchToEasyMDE();
} else { } else {
this.switchToTextarea(); this.switchToTextarea();
@ -304,7 +315,7 @@ export class ComboMarkdownEditor {
if (this.easyMDE) return; if (this.easyMDE) return;
// EasyMDE's CSS should be loaded via webpack config, otherwise our own styles can not overwrite the default styles. // EasyMDE's CSS should be loaded via webpack config, otherwise our own styles can not overwrite the default styles.
const {default: EasyMDE} = await import(/* webpackChunkName: "easymde" */'easymde'); const {default: EasyMDE} = await import(/* webpackChunkName: "easymde" */'easymde');
const easyMDEOpt = { const easyMDEOpt: EasyMDE.Options = {
autoDownloadFontAwesome: false, autoDownloadFontAwesome: false,
element: this.textarea, element: this.textarea,
forceSync: true, forceSync: true,
@ -384,19 +395,20 @@ export class ComboMarkdownEditor {
} }
get userPreferredEditor() { get userPreferredEditor() {
return window.localStorage.getItem(`markdown-editor-${this.options.useScene ?? 'default'}`); return window.localStorage.getItem(`markdown-editor-${this.previewMode ?? 'default'}`);
} }
set userPreferredEditor(s) { set userPreferredEditor(s) {
window.localStorage.setItem(`markdown-editor-${this.options.useScene ?? 'default'}`, s); window.localStorage.setItem(`markdown-editor-${this.previewMode ?? 'default'}`, s);
} }
} }
export function getComboMarkdownEditor(el) { export function getComboMarkdownEditor(el) {
if (el instanceof $) el = el[0]; if (!el) return null;
return el?._giteaComboMarkdownEditor; if (el.length) el = el[0];
return el._giteaComboMarkdownEditor;
} }
export async function initComboMarkdownEditor(container: HTMLElement, options = {}) { export async function initComboMarkdownEditor(container: HTMLElement, options:ComboMarkdownEditorOptions = {}) {
if (!container) { if (!container) {
throw new Error('initComboMarkdownEditor: container is null'); throw new Error('initComboMarkdownEditor: container is null');
} }

View File

@ -5,7 +5,7 @@ import {fomanticQuery} from '../../modules/fomantic/base.ts';
const {i18n} = window.config; const {i18n} = window.config;
export function confirmModal({header = '', content = '', confirmButtonColor = 'primary'} = {}) { export function confirmModal({header = '', content = '', confirmButtonColor = 'primary'} = {}): Promise<boolean> {
return new Promise((resolve) => { return new Promise((resolve) => {
const headerHtml = header ? `<div class="header">${htmlEscape(header)}</div>` : ''; const headerHtml = header ? `<div class="header">${htmlEscape(header)}</div>` : '';
const modal = createElementFromHTML(` const modal = createElementFromHTML(`

View File

@ -1,4 +1,3 @@
import $ from 'jquery';
import {htmlEscape} from 'escape-goat'; import {htmlEscape} from 'escape-goat';
import {createCodeEditor} from './codeeditor.ts'; import {createCodeEditor} from './codeeditor.ts';
import {hideElem, queryElems, showElem, createElementFromHTML} from '../utils/dom.ts'; import {hideElem, queryElems, showElem, createElementFromHTML} from '../utils/dom.ts';
@ -6,39 +5,33 @@ import {initMarkupContent} from '../markup/content.ts';
import {attachRefIssueContextPopup} from './contextpopup.ts'; import {attachRefIssueContextPopup} from './contextpopup.ts';
import {POST} from '../modules/fetch.ts'; import {POST} from '../modules/fetch.ts';
import {initDropzone} from './dropzone.ts'; import {initDropzone} from './dropzone.ts';
import {confirmModal} from './comp/ConfirmModal.ts';
import {applyAreYouSure} from '../vendor/jquery.are-you-sure.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
function initEditPreviewTab($form) { function initEditPreviewTab(elForm: HTMLFormElement) {
const $tabMenu = $form.find('.repo-editor-menu'); const elTabMenu = elForm.querySelector('.repo-editor-menu');
$tabMenu.find('.item').tab(); fomanticQuery(elTabMenu.querySelectorAll('.item')).tab();
const $previewTab = $tabMenu.find('a[data-tab="preview"]');
if ($previewTab.length) {
$previewTab.on('click', async function () {
const $this = $(this);
let context = `${$this.data('context')}/`;
const mode = $this.data('markup-mode') || 'comment';
const $treePathEl = $form.find('input#tree_path');
if ($treePathEl.length > 0) {
context += $treePathEl.val();
}
context = context.substring(0, context.lastIndexOf('/'));
const formData = new FormData(); const elPreviewTab = elTabMenu.querySelector('a[data-tab="preview"]');
formData.append('mode', mode); const elPreviewPanel = elForm.querySelector('.tab[data-tab="preview"]');
formData.append('context', context); if (!elPreviewTab || !elPreviewPanel) return;
formData.append('text', $form.find('.tab[data-tab="write"] textarea').val());
formData.append('file_path', $treePathEl.val()); elPreviewTab.addEventListener('click', async () => {
try { const elTreePath = elForm.querySelector<HTMLInputElement>('input#tree_path');
const response = await POST($this.data('url'), {data: formData}); const previewUrl = elPreviewTab.getAttribute('data-preview-url');
const data = await response.text(); const previewContextRef = elPreviewTab.getAttribute('data-preview-context-ref');
const $previewPanel = $form.find('.tab[data-tab="preview"]'); let previewContext = `${previewContextRef}/${elTreePath.value}`;
if ($previewPanel.length) { previewContext = previewContext.substring(0, previewContext.lastIndexOf('/'));
renderPreviewPanelContent($previewPanel, data); const formData = new FormData();
} formData.append('mode', 'file');
} catch (error) { formData.append('context', previewContext);
console.error('Error:', error); formData.append('text', elForm.querySelector<HTMLTextAreaElement>('.tab[data-tab="write"] textarea').value);
} formData.append('file_path', elTreePath.value);
}); const response = await POST(previewUrl, {data: formData});
} const data = await response.text();
renderPreviewPanelContent(elPreviewPanel, data);
});
} }
export function initRepoEditor() { export function initRepoEditor() {
@ -151,8 +144,8 @@ export function initRepoEditor() {
} }
}); });
const $form = $('.repository.editor .edit.form'); const elForm = document.querySelector<HTMLFormElement>('.repository.editor .edit.form');
initEditPreviewTab($form); initEditPreviewTab(elForm);
(async () => { (async () => {
const editor = await createCodeEditor(editArea, filenameInput); const editor = await createCodeEditor(editArea, filenameInput);
@ -160,16 +153,16 @@ export function initRepoEditor() {
// Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage
// to enable or disable the commit button // to enable or disable the commit button
const commitButton = document.querySelector<HTMLButtonElement>('#commit-button'); const commitButton = document.querySelector<HTMLButtonElement>('#commit-button');
const $editForm = $('.ui.edit.form');
const dirtyFileClass = 'dirty-file'; const dirtyFileClass = 'dirty-file';
// Disabling the button at the start // Disabling the button at the start
if ($('input[name="page_has_posted"]').val() !== 'true') { if (document.querySelector<HTMLInputElement>('input[name="page_has_posted"]').value !== 'true') {
commitButton.disabled = true; commitButton.disabled = true;
} }
// Registering a custom listener for the file path and the file content // Registering a custom listener for the file path and the file content
$editForm.areYouSure({ // FIXME: it is not quite right here (old bug), it causes double-init, the global areYouSure "dirty" class will also be added
applyAreYouSure(elForm, {
silent: true, silent: true,
dirtyClass: dirtyFileClass, dirtyClass: dirtyFileClass,
fieldSelector: ':input:not(.commit-form-wrapper :input)', fieldSelector: ':input:not(.commit-form-wrapper :input)',
@ -187,24 +180,24 @@ export function initRepoEditor() {
editor.setValue(value); editor.setValue(value);
} }
commitButton?.addEventListener('click', (e) => { commitButton?.addEventListener('click', async (e) => {
// A modal which asks if an empty file should be committed // A modal which asks if an empty file should be committed
if (!editArea.value) { if (!editArea.value) {
$('#edit-empty-content-modal').modal({
onApprove() {
$('.edit.form').trigger('submit');
},
}).modal('show');
e.preventDefault(); e.preventDefault();
if (await confirmModal({
header: elForm.getAttribute('data-text-empty-confirm-header'),
content: elForm.getAttribute('data-text-empty-confirm-content'),
})) {
elForm.classList.remove('dirty');
elForm.submit();
}
} }
}); });
})(); })();
} }
export function renderPreviewPanelContent($previewPanel, data) { export function renderPreviewPanelContent(previewPanel: Element, content: string) {
$previewPanel.html(data); previewPanel.innerHTML = content;
initMarkupContent(); initMarkupContent();
attachRefIssueContextPopup(previewPanel.querySelectorAll('p .ref-issue'));
const $refIssues = $previewPanel.find('p .ref-issue');
attachRefIssueContextPopup($refIssues);
} }

View File

@ -90,6 +90,7 @@ function filterRepoFiles(filter) {
const span = document.createElement('span'); const span = document.createElement('span');
// safely escape by using textContent // safely escape by using textContent
span.textContent = part; span.textContent = part;
span.title = span.textContent;
// if the target file path is "abc/xyz", to search "bx", then the matchResult is ['a', 'b', 'c/', 'x', 'yz'] // if the target file path is "abc/xyz", to search "bx", then the matchResult is ['a', 'b', 'c/', 'x', 'yz']
// the matchResult[odd] is matched and highlighted to red. // the matchResult[odd] is matched and highlighted to red.
if (index % 2 === 1) span.classList.add('ui', 'text', 'red'); if (index % 2 === 1) span.classList.add('ui', 'text', 'red');

View File

@ -414,11 +414,6 @@ export function initRepoPullRequestReview() {
await handleReply(this); await handleReply(this);
}); });
const elReviewBox = document.querySelector('.review-box-panel');
if (elReviewBox) {
initComboMarkdownEditor(elReviewBox.querySelector('.combo-markdown-editor'));
}
// The following part is only for diff views // The following part is only for diff views
if (!$('.repository.pull.diff').length) return; if (!$('.repository.pull.diff').length) return;

View File

@ -1,5 +1,4 @@
import {hideElem, showElem} from '../utils/dom.ts'; import {hideElem, showElem} from '../utils/dom.ts';
import {initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
export function initRepoRelease() { export function initRepoRelease() {
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
@ -16,7 +15,6 @@ export function initRepoReleaseNew() {
if (!document.querySelector('.repository.new.release')) return; if (!document.querySelector('.repository.new.release')) return;
initTagNameEditor(); initTagNameEditor();
initRepoReleaseEditor();
} }
function initTagNameEditor() { function initTagNameEditor() {
@ -48,11 +46,3 @@ function initTagNameEditor() {
hideTargetInput(e.target); hideTargetInput(e.target);
}); });
} }
function initRepoReleaseEditor() {
const editor = document.querySelector<HTMLElement>('.repository.new.release .combo-markdown-editor');
if (!editor) {
return;
}
initComboMarkdownEditor(editor);
}

View File

@ -2,6 +2,7 @@ import {initMarkupContent} from '../markup/content.ts';
import {validateTextareaNonEmpty, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts'; import {validateTextareaNonEmpty, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
import {fomanticMobileScreen} from '../modules/fomantic.ts'; import {fomanticMobileScreen} from '../modules/fomantic.ts';
import {POST} from '../modules/fetch.ts'; import {POST} from '../modules/fetch.ts';
import type {ComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
async function initRepoWikiFormEditor() { async function initRepoWikiFormEditor() {
const editArea = document.querySelector<HTMLTextAreaElement>('.repository.wiki .combo-markdown-editor textarea'); const editArea = document.querySelector<HTMLTextAreaElement>('.repository.wiki .combo-markdown-editor textarea');
@ -9,7 +10,7 @@ async function initRepoWikiFormEditor() {
const form = document.querySelector('.repository.wiki.new .ui.form'); const form = document.querySelector('.repository.wiki.new .ui.form');
const editorContainer = form.querySelector<HTMLElement>('.combo-markdown-editor'); const editorContainer = form.querySelector<HTMLElement>('.combo-markdown-editor');
let editor; let editor: ComboMarkdownEditor;
let renderRequesting = false; let renderRequesting = false;
let lastContent; let lastContent;
@ -45,12 +46,10 @@ async function initRepoWikiFormEditor() {
renderEasyMDEPreview(); renderEasyMDEPreview();
editor = await initComboMarkdownEditor(editorContainer, { editor = await initComboMarkdownEditor(editorContainer, {
useScene: 'wiki',
// EasyMDE has some problems of height definition, it has inline style height 300px by default, so we also use inline styles to override it. // EasyMDE has some problems of height definition, it has inline style height 300px by default, so we also use inline styles to override it.
// And another benefit is that we only need to write the style once for both editors. // And another benefit is that we only need to write the style once for both editors.
// TODO: Move height style to CSS after EasyMDE removal. // TODO: Move height style to CSS after EasyMDE removal.
editorHeights: {minHeight: '300px', height: 'calc(100vh - 600px)'}, editorHeights: {minHeight: '300px', height: 'calc(100vh - 600px)'},
previewMode: 'wiki',
easyMDEOptions: { easyMDEOptions: {
previewRender: (_content, previewTarget) => previewTarget.innerHTML, // disable builtin preview render previewRender: (_content, previewTarget) => previewTarget.innerHTML, // disable builtin preview render
toolbar: ['bold', 'italic', 'strikethrough', '|', toolbar: ['bold', 'italic', 'strikethrough', '|',
@ -59,7 +58,7 @@ async function initRepoWikiFormEditor() {
'unordered-list', 'ordered-list', '|', 'unordered-list', 'ordered-list', '|',
'link', 'image', 'table', 'horizontal-rule', '|', 'link', 'image', 'table', 'horizontal-rule', '|',
'preview', 'fullscreen', 'side-by-side', '|', 'gitea-switch-to-textarea', 'preview', 'fullscreen', 'side-by-side', '|', 'gitea-switch-to-textarea',
], ] as any, // to use custom toolbar buttons
}, },
}); });

View File

@ -83,7 +83,11 @@ import {
initGlobalButtons, initGlobalButtons,
initGlobalDeleteButton, initGlobalDeleteButton,
} from './features/common-button.ts'; } from './features/common-button.ts';
import {initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts'; import {
initGlobalComboMarkdownEditor,
initGlobalEnterQuickSubmit,
initGlobalFormDirtyLeaveConfirm,
} from './features/common-form.ts';
initGiteaFomantic(); initGiteaFomantic();
initDirAuto(); initDirAuto();
@ -127,6 +131,7 @@ onDomReady(() => {
initGlobalCopyToClipboardListener, initGlobalCopyToClipboardListener,
initGlobalEnterQuickSubmit, initGlobalEnterQuickSubmit,
initGlobalFormDirtyLeaveConfirm, initGlobalFormDirtyLeaveConfirm,
initGlobalComboMarkdownEditor,
initGlobalDeleteButton, initGlobalDeleteButton,
initCommonOrganization, initCommonOrganization,

View File

@ -196,6 +196,6 @@ export function initAreYouSure($) {
}; };
} }
export function applyAreYouSure(selector: string) { export function applyAreYouSure(selectorOrEl: string|Element|$, opts = {}) {
$(selector).areYouSure(); $(selectorOrEl).areYouSure(opts);
} }