mirror of
https://github.com/go-gitea/gitea.git
synced 2025-08-19 23:58:29 +00:00
Merge branch 'main' into feature/enhanced-workflow-runs-api
This commit is contained in:
commit
0a17b7aa64
@ -178,6 +178,15 @@ func WithTx(parentCtx context.Context, f func(ctx context.Context) error) error
|
||||
return txWithNoCheck(parentCtx, f)
|
||||
}
|
||||
|
||||
// WithTx2 is similar to WithTx, but it has two return values: result and error.
|
||||
func WithTx2[T any](parentCtx context.Context, f func(ctx context.Context) (T, error)) (ret T, errRet error) {
|
||||
errRet = WithTx(parentCtx, func(ctx context.Context) (errInner error) {
|
||||
ret, errInner = f(ctx)
|
||||
return errInner
|
||||
})
|
||||
return ret, errRet
|
||||
}
|
||||
|
||||
func txWithNoCheck(parentCtx context.Context, f func(ctx context.Context) error) error {
|
||||
sess := xormEngine.NewSession()
|
||||
defer sess.Close()
|
||||
|
@ -22,17 +22,22 @@ func (b *Blob) Name() string {
|
||||
return b.name
|
||||
}
|
||||
|
||||
// GetBlobContent Gets the limited content of the blob as raw text
|
||||
func (b *Blob) GetBlobContent(limit int64) (string, error) {
|
||||
// GetBlobBytes Gets the limited content of the blob
|
||||
func (b *Blob) GetBlobBytes(limit int64) ([]byte, error) {
|
||||
if limit <= 0 {
|
||||
return "", nil
|
||||
return nil, nil
|
||||
}
|
||||
dataRc, err := b.DataAsync()
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
defer dataRc.Close()
|
||||
buf, err := util.ReadWithLimit(dataRc, int(limit))
|
||||
return util.ReadWithLimit(dataRc, int(limit))
|
||||
}
|
||||
|
||||
// GetBlobContent Gets the limited content of the blob as raw text
|
||||
func (b *Blob) GetBlobContent(limit int64) (string, error) {
|
||||
buf, err := b.GetBlobBytes(limit)
|
||||
return string(buf), err
|
||||
}
|
||||
|
||||
@ -99,11 +104,9 @@ loop:
|
||||
|
||||
// GuessContentType guesses the content type of the blob.
|
||||
func (b *Blob) GuessContentType() (typesniffer.SniffedType, error) {
|
||||
r, err := b.DataAsync()
|
||||
buf, err := b.GetBlobBytes(typesniffer.SniffContentSize)
|
||||
if err != nil {
|
||||
return typesniffer.SniffedType{}, err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
return typesniffer.DetectContentTypeFromReader(r)
|
||||
return typesniffer.DetectContentType(buf), nil
|
||||
}
|
||||
|
@ -6,13 +6,14 @@ package console
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"path"
|
||||
"unicode/utf8"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/typesniffer"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
trend "github.com/buildkite/terminal-to-html/v3"
|
||||
"github.com/go-enry/go-enry/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -22,6 +23,8 @@ func init() {
|
||||
// Renderer implements markup.Renderer
|
||||
type Renderer struct{}
|
||||
|
||||
var _ markup.RendererContentDetector = (*Renderer)(nil)
|
||||
|
||||
// Name implements markup.Renderer
|
||||
func (Renderer) Name() string {
|
||||
return "console"
|
||||
@ -40,15 +43,36 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule {
|
||||
}
|
||||
|
||||
// CanRender implements markup.RendererContentDetector
|
||||
func (Renderer) CanRender(filename string, input io.Reader) bool {
|
||||
buf, err := io.ReadAll(input)
|
||||
if err != nil {
|
||||
func (Renderer) CanRender(filename string, sniffedType typesniffer.SniffedType, prefetchBuf []byte) bool {
|
||||
if !sniffedType.IsTextPlain() {
|
||||
return false
|
||||
}
|
||||
if enry.GetLanguage(path.Base(filename), buf) != enry.OtherLanguage {
|
||||
|
||||
s := util.UnsafeBytesToString(prefetchBuf)
|
||||
rs := []rune(s)
|
||||
cnt := 0
|
||||
firstErrPos := -1
|
||||
isCtrlSep := func(p int) bool {
|
||||
return p < len(rs) && (rs[p] == ';' || rs[p] == 'm')
|
||||
}
|
||||
for i, c := range rs {
|
||||
if c == 0 {
|
||||
return false
|
||||
}
|
||||
if c == '\x1b' {
|
||||
match := i+1 < len(rs) && rs[i+1] == '['
|
||||
if match && (isCtrlSep(i+2) || isCtrlSep(i+3) || isCtrlSep(i+4) || isCtrlSep(i+5)) {
|
||||
cnt++
|
||||
}
|
||||
}
|
||||
if c == utf8.RuneError && firstErrPos == -1 {
|
||||
firstErrPos = i
|
||||
}
|
||||
}
|
||||
if firstErrPos != -1 && firstErrPos != len(rs)-1 {
|
||||
return false
|
||||
}
|
||||
return bytes.ContainsRune(buf, '\x1b')
|
||||
return cnt >= 2 // only render it as console output if there are at least two escape sequences
|
||||
}
|
||||
|
||||
// Render renders terminal colors to HTML with all specific handling stuff.
|
||||
|
@ -8,23 +8,39 @@ import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/typesniffer"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRenderConsole(t *testing.T) {
|
||||
var render Renderer
|
||||
kases := map[string]string{
|
||||
"\x1b[37m\x1b[40mnpm\x1b[0m \x1b[0m\x1b[32minfo\x1b[0m \x1b[0m\x1b[35mit worked if it ends with\x1b[0m ok": "<span class=\"term-fg37 term-bg40\">npm</span> <span class=\"term-fg32\">info</span> <span class=\"term-fg35\">it worked if it ends with</span> ok",
|
||||
cases := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"\x1b[37m\x1b[40mnpm\x1b[0m \x1b[0m\x1b[32minfo\x1b[0m \x1b[0m\x1b[35mit worked if it ends with\x1b[0m ok", `<span class="term-fg37 term-bg40">npm</span> <span class="term-fg32">info</span> <span class="term-fg35">it worked if it ends with</span> ok`},
|
||||
{"\x1b[1;2m \x1b[123m 啊", `<span class="term-fg2"> 啊</span>`},
|
||||
{"\x1b[1;2m \x1b[123m \xef", `<span class="term-fg2"> <20></span>`},
|
||||
{"\x1b[1;2m \x1b[123m \xef \xef", ``},
|
||||
{"\x1b[12", ``},
|
||||
{"\x1b[1", ``},
|
||||
{"\x1b[FOO\x1b[", ``},
|
||||
{"\x1b[mFOO\x1b[m", `FOO`},
|
||||
}
|
||||
|
||||
for k, v := range kases {
|
||||
var render Renderer
|
||||
for i, c := range cases {
|
||||
var buf strings.Builder
|
||||
canRender := render.CanRender("test", strings.NewReader(k))
|
||||
assert.True(t, canRender)
|
||||
st := typesniffer.DetectContentType([]byte(c.input))
|
||||
canRender := render.CanRender("test", st, []byte(c.input))
|
||||
if c.expected == "" {
|
||||
assert.False(t, canRender, "case %d: expected not to render", i)
|
||||
continue
|
||||
}
|
||||
|
||||
err := render.Render(markup.NewRenderContext(t.Context()), strings.NewReader(k), &buf)
|
||||
assert.True(t, canRender)
|
||||
err := render.Render(markup.NewRenderContext(t.Context()), strings.NewReader(c.input), &buf)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, v, buf.String())
|
||||
assert.Equal(t, c.expected, buf.String())
|
||||
}
|
||||
}
|
||||
|
@ -4,12 +4,12 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/typesniffer"
|
||||
)
|
||||
|
||||
// Renderer defines an interface for rendering markup file to HTML
|
||||
@ -37,7 +37,7 @@ type ExternalRenderer interface {
|
||||
// RendererContentDetector detects if the content can be rendered
|
||||
// by specified renderer
|
||||
type RendererContentDetector interface {
|
||||
CanRender(filename string, input io.Reader) bool
|
||||
CanRender(filename string, sniffedType typesniffer.SniffedType, prefetchBuf []byte) bool
|
||||
}
|
||||
|
||||
var (
|
||||
@ -60,13 +60,9 @@ func GetRendererByFileName(filename string) Renderer {
|
||||
}
|
||||
|
||||
// DetectRendererType detects the markup type of the content
|
||||
func DetectRendererType(filename string, input io.Reader) string {
|
||||
buf, err := io.ReadAll(input)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
func DetectRendererType(filename string, sniffedType typesniffer.SniffedType, prefetchBuf []byte) string {
|
||||
for _, renderer := range renderers {
|
||||
if detector, ok := renderer.(RendererContentDetector); ok && detector.CanRender(filename, bytes.NewReader(buf)) {
|
||||
if detector, ok := renderer.(RendererContentDetector); ok && detector.CanRender(filename, sniffedType, prefetchBuf) {
|
||||
return renderer.Name()
|
||||
}
|
||||
}
|
||||
|
@ -8,8 +8,11 @@ import "time"
|
||||
|
||||
// CreateUserOption create user options
|
||||
type CreateUserOption struct {
|
||||
SourceID int64 `json:"source_id"`
|
||||
SourceID int64 `json:"source_id"`
|
||||
// identifier of the user, provided by the external authenticator (if configured)
|
||||
// default: empty
|
||||
LoginName string `json:"login_name"`
|
||||
// username of the user
|
||||
// required: true
|
||||
Username string `json:"username" binding:"Required;Username;MaxSize(40)"`
|
||||
FullName string `json:"full_name" binding:"MaxSize(100)"`
|
||||
@ -32,6 +35,8 @@ type CreateUserOption struct {
|
||||
type EditUserOption struct {
|
||||
// required: true
|
||||
SourceID int64 `json:"source_id"`
|
||||
// identifier of the user, provided by the external authenticator (if configured)
|
||||
// default: empty
|
||||
// required: true
|
||||
LoginName string `json:"login_name" binding:"Required"`
|
||||
// swagger:strfmt email
|
||||
|
@ -71,7 +71,8 @@ type PayloadUser struct {
|
||||
// Full name of the commit author
|
||||
Name string `json:"name"`
|
||||
// swagger:strfmt email
|
||||
Email string `json:"email"`
|
||||
Email string `json:"email"`
|
||||
// username of the user
|
||||
UserName string `json:"username"`
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ type AddTimeOption struct {
|
||||
Time int64 `json:"time" binding:"Required"`
|
||||
// swagger:strfmt date-time
|
||||
Created time.Time `json:"created"`
|
||||
// User who spent the time (optional)
|
||||
// username of the user who spent the time working on the issue (optional)
|
||||
User string `json:"user_name"`
|
||||
}
|
||||
|
||||
@ -26,7 +26,8 @@ type TrackedTime struct {
|
||||
// Time in seconds
|
||||
Time int64 `json:"time"`
|
||||
// deprecated (only for backwards compatibility)
|
||||
UserID int64 `json:"user_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
// username of the user
|
||||
UserName string `json:"user_name"`
|
||||
// deprecated (only for backwards compatibility)
|
||||
IssueID int64 `json:"issue_id"`
|
||||
|
@ -15,6 +15,7 @@ type Organization struct {
|
||||
Location string `json:"location"`
|
||||
Visibility string `json:"visibility"`
|
||||
RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"`
|
||||
// username of the organization
|
||||
// deprecated
|
||||
UserName string `json:"username"`
|
||||
}
|
||||
@ -30,6 +31,7 @@ type OrganizationPermissions struct {
|
||||
|
||||
// CreateOrgOption options for creating an organization
|
||||
type CreateOrgOption struct {
|
||||
// username of the organization
|
||||
// required: true
|
||||
UserName string `json:"username" binding:"Required;Username;MaxSize(40)"`
|
||||
FullName string `json:"full_name" binding:"MaxSize(100)"`
|
||||
|
@ -15,9 +15,9 @@ import (
|
||||
type User struct {
|
||||
// the user's id
|
||||
ID int64 `json:"id"`
|
||||
// the user's username
|
||||
// login of the user, same as `username`
|
||||
UserName string `json:"login"`
|
||||
// the user's authentication sign-in name.
|
||||
// identifier of the user, provided by the external authenticator (if configured)
|
||||
// default: empty
|
||||
LoginName string `json:"login_name"`
|
||||
// The ID of the user's Authentication Source
|
||||
|
@ -11,6 +11,7 @@ type Email struct {
|
||||
Verified bool `json:"verified"`
|
||||
Primary bool `json:"primary"`
|
||||
UserID int64 `json:"user_id"`
|
||||
// username of the user
|
||||
UserName string `json:"username"`
|
||||
}
|
||||
|
||||
|
@ -6,18 +6,14 @@ package typesniffer
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Use at most this many bytes to determine Content Type.
|
||||
const sniffLen = 1024
|
||||
const SniffContentSize = 1024
|
||||
|
||||
const (
|
||||
MimeTypeImageSvg = "image/svg+xml"
|
||||
@ -26,22 +22,30 @@ const (
|
||||
MimeTypeApplicationOctetStream = "application/octet-stream"
|
||||
)
|
||||
|
||||
var (
|
||||
svgComment = regexp.MustCompile(`(?s)<!--.*?-->`)
|
||||
svgTagRegex = regexp.MustCompile(`(?si)\A\s*(?:(<!DOCTYPE\s+svg([\s:]+.*?>|>))\s*)*<svg\b`)
|
||||
svgTagInXMLRegex = regexp.MustCompile(`(?si)\A<\?xml\b.*?\?>\s*(?:(<!DOCTYPE\s+svg([\s:]+.*?>|>))\s*)*<svg\b`)
|
||||
)
|
||||
var globalVars = sync.OnceValue(func() (ret struct {
|
||||
svgComment, svgTagRegex, svgTagInXMLRegex *regexp.Regexp
|
||||
},
|
||||
) {
|
||||
ret.svgComment = regexp.MustCompile(`(?s)<!--.*?-->`)
|
||||
ret.svgTagRegex = regexp.MustCompile(`(?si)\A\s*(?:(<!DOCTYPE\s+svg([\s:]+.*?>|>))\s*)*<svg\b`)
|
||||
ret.svgTagInXMLRegex = regexp.MustCompile(`(?si)\A<\?xml\b.*?\?>\s*(?:(<!DOCTYPE\s+svg([\s:]+.*?>|>))\s*)*<svg\b`)
|
||||
return ret
|
||||
})
|
||||
|
||||
// SniffedType contains information about a blobs type.
|
||||
// SniffedType contains information about a blob's type.
|
||||
type SniffedType struct {
|
||||
contentType string
|
||||
}
|
||||
|
||||
// IsText etects if content format is plain text.
|
||||
// IsText detects if the content format is text family, including text/plain, text/html, text/css, etc.
|
||||
func (ct SniffedType) IsText() bool {
|
||||
return strings.Contains(ct.contentType, "text/")
|
||||
}
|
||||
|
||||
func (ct SniffedType) IsTextPlain() bool {
|
||||
return strings.Contains(ct.contentType, "text/plain")
|
||||
}
|
||||
|
||||
// IsImage detects if data is an image format
|
||||
func (ct SniffedType) IsImage() bool {
|
||||
return strings.Contains(ct.contentType, "image/")
|
||||
@ -57,12 +61,12 @@ func (ct SniffedType) IsPDF() bool {
|
||||
return strings.Contains(ct.contentType, "application/pdf")
|
||||
}
|
||||
|
||||
// IsVideo detects if data is an video format
|
||||
// IsVideo detects if data is a video format
|
||||
func (ct SniffedType) IsVideo() bool {
|
||||
return strings.Contains(ct.contentType, "video/")
|
||||
}
|
||||
|
||||
// IsAudio detects if data is an video format
|
||||
// IsAudio detects if data is a video format
|
||||
func (ct SniffedType) IsAudio() bool {
|
||||
return strings.Contains(ct.contentType, "audio/")
|
||||
}
|
||||
@ -103,33 +107,34 @@ func detectFileTypeBox(data []byte) (brands []string, found bool) {
|
||||
return brands, true
|
||||
}
|
||||
|
||||
// DetectContentType extends http.DetectContentType with more content types. Defaults to text/unknown if input is empty.
|
||||
// DetectContentType extends http.DetectContentType with more content types. Defaults to text/plain if input is empty.
|
||||
func DetectContentType(data []byte) SniffedType {
|
||||
if len(data) == 0 {
|
||||
return SniffedType{"text/unknown"}
|
||||
return SniffedType{"text/plain"}
|
||||
}
|
||||
|
||||
ct := http.DetectContentType(data)
|
||||
|
||||
if len(data) > sniffLen {
|
||||
data = data[:sniffLen]
|
||||
if len(data) > SniffContentSize {
|
||||
data = data[:SniffContentSize]
|
||||
}
|
||||
|
||||
vars := globalVars()
|
||||
// SVG is unsupported by http.DetectContentType, https://github.com/golang/go/issues/15888
|
||||
detectByHTML := strings.Contains(ct, "text/plain") || strings.Contains(ct, "text/html")
|
||||
detectByXML := strings.Contains(ct, "text/xml")
|
||||
if detectByHTML || detectByXML {
|
||||
dataProcessed := svgComment.ReplaceAll(data, nil)
|
||||
dataProcessed := vars.svgComment.ReplaceAll(data, nil)
|
||||
dataProcessed = bytes.TrimSpace(dataProcessed)
|
||||
if detectByHTML && svgTagRegex.Match(dataProcessed) ||
|
||||
detectByXML && svgTagInXMLRegex.Match(dataProcessed) {
|
||||
if detectByHTML && vars.svgTagRegex.Match(dataProcessed) ||
|
||||
detectByXML && vars.svgTagInXMLRegex.Match(dataProcessed) {
|
||||
ct = MimeTypeImageSvg
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(ct, "audio/") && bytes.HasPrefix(data, []byte("ID3")) {
|
||||
// The MP3 detection is quite inaccurate, any content with "ID3" prefix will result in "audio/mpeg".
|
||||
// So remove the "ID3" prefix and detect again, if result is text, then it must be text content.
|
||||
// So remove the "ID3" prefix and detect again, then if the result is "text", it must be text content.
|
||||
// This works especially because audio files contain many unprintable/invalid characters like `0x00`
|
||||
ct2 := http.DetectContentType(data[3:])
|
||||
if strings.HasPrefix(ct2, "text/") {
|
||||
@ -155,15 +160,3 @@ func DetectContentType(data []byte) SniffedType {
|
||||
}
|
||||
return SniffedType{ct}
|
||||
}
|
||||
|
||||
// DetectContentTypeFromReader guesses the content type contained in the reader.
|
||||
func DetectContentTypeFromReader(r io.Reader) (SniffedType, error) {
|
||||
buf := make([]byte, sniffLen)
|
||||
n, err := util.ReadAtMost(r, buf)
|
||||
if err != nil {
|
||||
return SniffedType{}, fmt.Errorf("DetectContentTypeFromReader io error: %w", err)
|
||||
}
|
||||
buf = buf[:n]
|
||||
|
||||
return DetectContentType(buf), nil
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
package typesniffer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
@ -17,7 +16,7 @@ func TestDetectContentTypeLongerThanSniffLen(t *testing.T) {
|
||||
// Pre-condition: Shorter than sniffLen detects SVG.
|
||||
assert.Equal(t, "image/svg+xml", DetectContentType([]byte(`<!-- Comment --><svg></svg>`)).contentType)
|
||||
// Longer than sniffLen detects something else.
|
||||
assert.NotEqual(t, "image/svg+xml", DetectContentType([]byte(`<!-- `+strings.Repeat("x", sniffLen)+` --><svg></svg>`)).contentType)
|
||||
assert.NotEqual(t, "image/svg+xml", DetectContentType([]byte(`<!-- `+strings.Repeat("x", SniffContentSize)+` --><svg></svg>`)).contentType)
|
||||
}
|
||||
|
||||
func TestIsTextFile(t *testing.T) {
|
||||
@ -116,22 +115,13 @@ func TestIsAudio(t *testing.T) {
|
||||
assert.True(t, DetectContentType([]byte("ID3Toy\n====\t* hi 🌞, ..."+"🌛"[0:2])).IsText()) // test ID3 tag with incomplete UTF8 char
|
||||
}
|
||||
|
||||
func TestDetectContentTypeFromReader(t *testing.T) {
|
||||
mp3, _ := base64.StdEncoding.DecodeString("SUQzBAAAAAABAFRYWFgAAAASAAADbWFqb3JfYnJhbmQAbXA0MgBUWFhYAAAAEQAAA21pbm9yX3Zl")
|
||||
st, err := DetectContentTypeFromReader(bytes.NewReader(mp3))
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, st.IsAudio())
|
||||
}
|
||||
|
||||
func TestDetectContentTypeOgg(t *testing.T) {
|
||||
oggAudio, _ := hex.DecodeString("4f67675300020000000000000000352f0000000000007dc39163011e01766f72626973000000000244ac0000000000000071020000000000b8014f6767530000")
|
||||
st, err := DetectContentTypeFromReader(bytes.NewReader(oggAudio))
|
||||
assert.NoError(t, err)
|
||||
st := DetectContentType(oggAudio)
|
||||
assert.True(t, st.IsAudio())
|
||||
|
||||
oggVideo, _ := hex.DecodeString("4f676753000200000000000000007d9747ef000000009b59daf3012a807468656f7261030201001e00110001e000010e00020000001e00000001000001000001")
|
||||
st, err = DetectContentTypeFromReader(bytes.NewReader(oggVideo))
|
||||
assert.NoError(t, err)
|
||||
st = DetectContentType(oggVideo)
|
||||
assert.True(t, st.IsVideo())
|
||||
}
|
||||
|
||||
|
@ -1917,7 +1917,6 @@ pulls.cmd_instruction_checkout_title=Checkout
|
||||
pulls.cmd_instruction_checkout_desc=Z vašeho repositáře projektu se podívejte na novou větev a vyzkoušejte změny.
|
||||
pulls.cmd_instruction_merge_title=Sloučit
|
||||
pulls.cmd_instruction_merge_desc=Slučte změny a aktualizujte je na Gitea.
|
||||
pulls.cmd_instruction_merge_warning=Varování: Tato operace nemůže sloučit požadavek na natažení, protože „autodetekce manuálních sloučení“ nebyla povolena
|
||||
pulls.clear_merge_message=Vymazat zprávu o sloučení
|
||||
pulls.clear_merge_message_hint=Vymazání zprávy o sloučení odstraní pouze obsah zprávy a ponechá generované přídavky gitu jako "Co-AuthoreBy …".
|
||||
|
||||
|
@ -1953,7 +1953,6 @@ pulls.cmd_instruction_checkout_title=Checkout
|
||||
pulls.cmd_instruction_checkout_desc=Wechsle auf einen neuen Branch in deinem lokalen Repository und teste die Änderungen.
|
||||
pulls.cmd_instruction_merge_title=Mergen
|
||||
pulls.cmd_instruction_merge_desc=Die Änderungen mergen und auf Gitea aktualisieren.
|
||||
pulls.cmd_instruction_merge_warning=Warnung: Dieser Vorgang kann den Pull-Request nicht mergen, da "manueller Merge" nicht aktiviert wurde
|
||||
pulls.clear_merge_message=Merge-Nachricht löschen
|
||||
pulls.clear_merge_message_hint=Das Löschen der Merge-Nachricht wird nur den Inhalt der Commit-Nachricht entfernen und generierte Git-Trailer wie "Co-Authored-By …" erhalten.
|
||||
|
||||
|
@ -1969,7 +1969,7 @@ pulls.cmd_instruction_checkout_title = Checkout
|
||||
pulls.cmd_instruction_checkout_desc = From your project repository, check out a new branch and test the changes.
|
||||
pulls.cmd_instruction_merge_title = Merge
|
||||
pulls.cmd_instruction_merge_desc = Merge the changes and update on Gitea.
|
||||
pulls.cmd_instruction_merge_warning = Warning: This operation can not merge pull request because "autodetect manual merge" was not enable
|
||||
pulls.cmd_instruction_merge_warning = Warning: This operation cannot merge pull request because "autodetect manual merge" is not enabled.
|
||||
pulls.clear_merge_message = Clear merge message
|
||||
pulls.clear_merge_message_hint = Clearing the merge message will only remove the commit message content and keep generated git trailers such as "Co-Authored-By …".
|
||||
|
||||
|
@ -1969,7 +1969,6 @@ pulls.cmd_instruction_checkout_title=Basculer
|
||||
pulls.cmd_instruction_checkout_desc=Depuis votre dépôt, basculer sur une nouvelle branche et tester des modifications.
|
||||
pulls.cmd_instruction_merge_title=Fusionner
|
||||
pulls.cmd_instruction_merge_desc=Fusionner les modifications et mettre à jour sur Gitea.
|
||||
pulls.cmd_instruction_merge_warning=Attention : cette opération ne peut pas fusionner la demande d’ajout car la « détection automatique de fusion manuelle » n’a pas été activée
|
||||
pulls.clear_merge_message=Effacer le message de fusion
|
||||
pulls.clear_merge_message_hint=Effacer le message de fusion ne supprimera que le message de la révision, mais pas les pieds de révision générés tels que "Co-Authored-By:".
|
||||
|
||||
|
@ -1969,7 +1969,6 @@ pulls.cmd_instruction_checkout_title=Seiceáil
|
||||
pulls.cmd_instruction_checkout_desc=Ó stór tionscadail, seiceáil brainse nua agus déan tástáil ar na hathruithe.
|
||||
pulls.cmd_instruction_merge_title=Cumaisc
|
||||
pulls.cmd_instruction_merge_desc=Cumaisc na hathruithe agus nuashonrú ar Gitea.
|
||||
pulls.cmd_instruction_merge_warning=Rabhadh: Ní féidir leis an oibríocht seo an t-iarratas tarraingthe a chumasc toisc nach raibh "cumasc láimhe uathoibríoch" cumasaithe
|
||||
pulls.clear_merge_message=Glan an teachtaireacht chumaisc
|
||||
pulls.clear_merge_message_hint=Má imrítear an teachtaireacht chumaisc ní bhainfear ach ábhar na teachtaireachta tiomanta agus coimeádfar leantóirí git ginte ar nós "Co-Authored-By …".
|
||||
|
||||
|
@ -1960,7 +1960,6 @@ pulls.cmd_instruction_checkout_title=チェックアウト
|
||||
pulls.cmd_instruction_checkout_desc=プロジェクトリポジトリから新しいブランチをチェックアウトし、変更内容をテストします。
|
||||
pulls.cmd_instruction_merge_title=マージ
|
||||
pulls.cmd_instruction_merge_desc=変更内容をマージして、Giteaに反映します。
|
||||
pulls.cmd_instruction_merge_warning=警告: 「手動マージの自動検出」が有効ではないため、この操作ではプルリクエストをマージできません
|
||||
pulls.clear_merge_message=マージメッセージをクリア
|
||||
pulls.clear_merge_message_hint=マージメッセージのクリアは、コミットメッセージの除去だけを行います。 生成されたGitトレーラー("Co-Authored-By …" 等)はそのまま残ります。
|
||||
|
||||
|
@ -1969,7 +1969,6 @@ pulls.cmd_instruction_checkout_title=Checkout
|
||||
pulls.cmd_instruction_checkout_desc=A partir do seu repositório, crie um novo ramo e teste nele as modificações.
|
||||
pulls.cmd_instruction_merge_title=Integrar
|
||||
pulls.cmd_instruction_merge_desc=Integrar as modificações e enviar para o Gitea.
|
||||
pulls.cmd_instruction_merge_warning=Aviso: Esta operação não pode executar pedidos de integração porque "auto-identificar integração manual" não estava habilitado
|
||||
pulls.clear_merge_message=Apagar mensagem de integração
|
||||
pulls.clear_merge_message_hint=Apagar a mensagem de integração apenas remove o conteúdo da mensagem de cometimento e mantém os rodapés do git, tais como "Co-Autorado-Por …".
|
||||
|
||||
@ -2384,6 +2383,7 @@ settings.event_repository=Repositório
|
||||
settings.event_repository_desc=Repositório criado ou eliminado.
|
||||
settings.event_header_issue=Eventos da questão
|
||||
settings.event_issues=Questões
|
||||
settings.event_issues_desc=Questão aberta, fechada, reaberta, editada ou eliminada.
|
||||
settings.event_issue_assign=Questão atribuída
|
||||
settings.event_issue_assign_desc=Encarregado atribuído ou retirado à questão.
|
||||
settings.event_issue_label=Questão com rótulo
|
||||
@ -2394,6 +2394,7 @@ settings.event_issue_comment=Comentário da questão
|
||||
settings.event_issue_comment_desc=Comentário da questão criado, editado ou eliminado.
|
||||
settings.event_header_pull_request=Eventos de pedidos de integração
|
||||
settings.event_pull_request=Pedido de integração
|
||||
settings.event_pull_request_desc=Pedido de integração aberto, fechado, reaberto, editado ou eliminado.
|
||||
settings.event_pull_request_assign=Encarregado atribuído ao pedido de integração
|
||||
settings.event_pull_request_assign_desc=Encarregado atribuído ou retirado ao pedido de integração.
|
||||
settings.event_pull_request_label=Rótulo atribuído ao pedido de integração
|
||||
|
@ -2256,6 +2256,7 @@ settings.event_repository=Репозиторій
|
||||
settings.event_repository_desc=Репозиторій створений або видалено.
|
||||
settings.event_header_issue=Події задачі
|
||||
settings.event_issues=Задачі
|
||||
settings.event_issues_desc=Задачу відкрито, закрито, повторно відкрито, відредаговано або видалено.
|
||||
settings.event_issue_assign=Задачу призначено
|
||||
settings.event_issue_assign_desc=Задачу призначено або скасовано.
|
||||
settings.event_issue_label_desc=Мітки задачі оновлено або видалено.
|
||||
@ -2263,6 +2264,7 @@ settings.event_issue_comment=Коментар задачі
|
||||
settings.event_issue_comment_desc=Коментар задачі створено, видалено чи відредаговано.
|
||||
settings.event_header_pull_request=Події запиту злиття
|
||||
settings.event_pull_request=Запити на злиття
|
||||
settings.event_pull_request_desc=Запит на злиття відкрито, закрито, повторно відкрито, відредаговано або видалено.
|
||||
settings.event_pull_request_assign=Запит на злиття призначено
|
||||
settings.event_pull_request_assign_desc=Запит на злиття призначено або скасовано.
|
||||
settings.event_pull_request_label=Запиту на злиття призначена мітка
|
||||
|
@ -1955,7 +1955,6 @@ pulls.cmd_instruction_checkout_title=检出
|
||||
pulls.cmd_instruction_checkout_desc=从您的仓库中检出一个新的分支并测试变更。
|
||||
pulls.cmd_instruction_merge_title=合并
|
||||
pulls.cmd_instruction_merge_desc=合并变更并更新到 Gitea 上
|
||||
pulls.cmd_instruction_merge_warning=警告:此操作不能合并该合并请求,因为「自动检测手动合并」未启用
|
||||
pulls.clear_merge_message=清除合并信息
|
||||
pulls.clear_merge_message_hint=清除合并消息只会删除提交消息内容,并保留生成的 Git 附加内容,如「Co-Authored-By…」。
|
||||
|
||||
|
@ -1901,7 +1901,6 @@ pulls.cmd_instruction_checkout_title=檢出
|
||||
pulls.cmd_instruction_checkout_desc=從您的專案儲存庫中,檢出一個新分支並測試變更。
|
||||
pulls.cmd_instruction_merge_title=合併
|
||||
pulls.cmd_instruction_merge_desc=合併變更並在 Gitea 上更新。
|
||||
pulls.cmd_instruction_merge_warning=警告:此操作無法合併合併請求,因為未啟用「自動檢測手動合併」
|
||||
pulls.clear_merge_message=清除合併訊息
|
||||
pulls.clear_merge_message_hint=清除合併訊息將僅移除提交訊息內容,留下產生的 git 結尾,如「Co-Authored-By …」。
|
||||
|
||||
|
51
package-lock.json
generated
51
package-lock.json
generated
@ -40,6 +40,7 @@
|
||||
"minimatch": "10.0.2",
|
||||
"monaco-editor": "0.52.2",
|
||||
"monaco-editor-webpack-plugin": "7.1.0",
|
||||
"online-3d-viewer": "0.16.0",
|
||||
"pdfobject": "2.3.1",
|
||||
"perfect-debounce": "1.0.0",
|
||||
"postcss": "8.5.5",
|
||||
@ -2026,6 +2027,16 @@
|
||||
"vue": "^3.2.29"
|
||||
}
|
||||
},
|
||||
"node_modules/@simonwep/pickr": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmmirror.com/@simonwep/pickr/-/pickr-1.9.0.tgz",
|
||||
"integrity": "sha512-oEYvv15PyfZzjoAzvXYt3UyNGwzsrpFxLaZKzkOSd0WYBVwLd19iJerePDONxC1iF6+DpcswPdLIM2KzCJuYFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-js": "3.32.2",
|
||||
"nanopop": "2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@stoplight/better-ajv-errors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz",
|
||||
@ -5337,6 +5348,17 @@
|
||||
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.32.2",
|
||||
"resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.32.2.tgz",
|
||||
"integrity": "sha512-pxXSw1mYZPDGvTQqEc5vgIb83jGQKFGYWY76z4a7weZXUolw3G+OvpZqSRcfYOoOVUQJYEPsWeQK8pKEnUtWxQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.43.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.43.0.tgz",
|
||||
@ -7721,6 +7743,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||
@ -10285,6 +10313,12 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/nanopop": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/nanopop/-/nanopop-2.3.0.tgz",
|
||||
"integrity": "sha512-fzN+T2K7/Ah25XU02MJkPZ5q4Tj5FpjmIYq4rvoHX4yb16HzFdCO6JxFFn5Y/oBhQ8no8fUZavnyIv9/+xkBBw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/napi-postinstall": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.2.4.tgz",
|
||||
@ -10525,6 +10559,17 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/online-3d-viewer": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmmirror.com/online-3d-viewer/-/online-3d-viewer-0.16.0.tgz",
|
||||
"integrity": "sha512-Mcmo41TM3K+svlMDRH8ySKSY2e8s7Sssdb5U9LV3gkFKVWGGuS304Vk5gqxopAJbE72DpsC67Ve3YNtcAuROwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@simonwep/pickr": "1.9.0",
|
||||
"fflate": "0.8.2",
|
||||
"three": "0.176.0"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
@ -13193,6 +13238,12 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.176.0",
|
||||
"resolved": "https://registry.npmmirror.com/three/-/three-0.176.0.tgz",
|
||||
"integrity": "sha512-PWRKYWQo23ojf9oZSlRGH8K09q7nRSWx6LY/HF/UUrMdYgN9i1e2OwJYHoQjwc6HF/4lvvYLC5YC1X8UJL2ZpA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/throttle-debounce": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
|
||||
|
@ -39,6 +39,7 @@
|
||||
"minimatch": "10.0.2",
|
||||
"monaco-editor": "0.52.2",
|
||||
"monaco-editor-webpack-plugin": "7.1.0",
|
||||
"online-3d-viewer": "0.16.0",
|
||||
"pdfobject": "2.3.1",
|
||||
"perfect-debounce": "1.0.0",
|
||||
"postcss": "8.5.5",
|
||||
|
@ -467,7 +467,9 @@ func CommonRoutes() *web.Router {
|
||||
g.MatchPath("HEAD", "/<group:*>/repodata/<filename>", rpm.CheckRepositoryFileExistence)
|
||||
g.MatchPath("GET", "/<group:*>/repodata/<filename>", rpm.GetRepositoryFile)
|
||||
g.MatchPath("PUT", "/<group:*>/upload", reqPackageAccess(perm.AccessModeWrite), rpm.UploadPackageFile)
|
||||
// this URL pattern is only used internally in the RPM index, it is generated by us, the filename part is not really used (can be anything)
|
||||
g.MatchPath("HEAD,GET", "/<group:*>/package/<name>/<version>/<architecture>", rpm.DownloadPackageFile)
|
||||
g.MatchPath("HEAD,GET", "/<group:*>/package/<name>/<version>/<architecture>/<filename>", rpm.DownloadPackageFile)
|
||||
g.MatchPath("DELETE", "/<group:*>/package/<name>/<version>/<architecture>", reqPackageAccess(perm.AccessModeWrite), rpm.DeletePackageFile)
|
||||
}, reqPackageAccess(perm.AccessModeRead))
|
||||
|
||||
|
@ -21,7 +21,7 @@ func (a *Auth) Name() string {
|
||||
}
|
||||
|
||||
// Verify extracts the user from the Bearer token
|
||||
// If it's an anonymous session a ghost user is returned
|
||||
// If it's an anonymous session, a ghost user is returned
|
||||
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) {
|
||||
packageMeta, err := packages.ParseAuthorizationRequest(req)
|
||||
if err != nil {
|
||||
|
@ -95,15 +95,13 @@ func containerGlobalLockKey(piOwnerID int64, piName, usage string) string {
|
||||
}
|
||||
|
||||
func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageInfo) (*packages_model.PackageVersion, error) {
|
||||
var uploadVersion *packages_model.PackageVersion
|
||||
|
||||
releaser, err := globallock.Lock(ctx, containerGlobalLockKey(pi.Owner.ID, pi.Name, "package"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer releaser()
|
||||
|
||||
err = db.WithTx(ctx, func(ctx context.Context) error {
|
||||
return db.WithTx2(ctx, func(ctx context.Context) (*packages_model.PackageVersion, error) {
|
||||
created := true
|
||||
p := &packages_model.Package{
|
||||
OwnerID: pi.Owner.ID,
|
||||
@ -115,7 +113,7 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI
|
||||
if p, err = packages_model.TryInsertPackage(ctx, p); err != nil {
|
||||
if !errors.Is(err, packages_model.ErrDuplicatePackage) {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
created = false
|
||||
}
|
||||
@ -123,7 +121,7 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI
|
||||
if created {
|
||||
if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository, strings.ToLower(pi.Owner.LowerName+"/"+pi.Name)); err != nil {
|
||||
log.Error("Error setting package property: %v", err)
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@ -138,16 +136,11 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI
|
||||
if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil {
|
||||
if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
uploadVersion = pv
|
||||
|
||||
return nil
|
||||
return pv, nil
|
||||
})
|
||||
|
||||
return uploadVersion, err
|
||||
}
|
||||
|
||||
func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, pb *packages_model.PackageBlob) error {
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
@ -39,10 +40,14 @@ import (
|
||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests
|
||||
const maxManifestSize = 10 * 1024 * 1024
|
||||
|
||||
var (
|
||||
imageNamePattern = regexp.MustCompile(`\A[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*\z`)
|
||||
referencePattern = regexp.MustCompile(`\A[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}\z`)
|
||||
)
|
||||
var globalVars = sync.OnceValue(func() (ret struct {
|
||||
imageNamePattern, referencePattern *regexp.Regexp
|
||||
},
|
||||
) {
|
||||
ret.imageNamePattern = regexp.MustCompile(`\A[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*\z`)
|
||||
ret.referencePattern = regexp.MustCompile(`\A[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}\z`)
|
||||
return ret
|
||||
})
|
||||
|
||||
type containerHeaders struct {
|
||||
Status int
|
||||
@ -84,9 +89,7 @@ func jsonResponse(ctx *context.Context, status int, obj any) {
|
||||
Status: status,
|
||||
ContentType: "application/json",
|
||||
})
|
||||
if err := json.NewEncoder(ctx.Resp).Encode(obj); err != nil {
|
||||
log.Error("JSON encode: %v", err)
|
||||
}
|
||||
_ = json.NewEncoder(ctx.Resp).Encode(obj) // ignore network errors
|
||||
}
|
||||
|
||||
func apiError(ctx *context.Context, status int, err error) {
|
||||
@ -134,7 +137,7 @@ func ReqContainerAccess(ctx *context.Context) {
|
||||
|
||||
// VerifyImageName is a middleware which checks if the image name is allowed
|
||||
func VerifyImageName(ctx *context.Context) {
|
||||
if !imageNamePattern.MatchString(ctx.PathParam("image")) {
|
||||
if !globalVars().imageNamePattern.MatchString(ctx.PathParam("image")) {
|
||||
apiErrorDefined(ctx, errNameInvalid)
|
||||
}
|
||||
}
|
||||
@ -216,7 +219,7 @@ func GetRepositoryList(ctx *context.Context) {
|
||||
if len(repositories) == n {
|
||||
v := url.Values{}
|
||||
if n > 0 {
|
||||
v.Add("n", strconv.Itoa(n))
|
||||
v.Add("n", strconv.Itoa(n)) // FIXME: "n" can't be zero here, the logic is inconsistent with GetTagsList
|
||||
}
|
||||
v.Add("last", repositories[len(repositories)-1])
|
||||
|
||||
@ -565,7 +568,7 @@ func PutManifest(ctx *context.Context) {
|
||||
IsTagged: digest.Digest(reference).Validate() != nil,
|
||||
}
|
||||
|
||||
if mci.IsTagged && !referencePattern.MatchString(reference) {
|
||||
if mci.IsTagged && !globalVars().referencePattern.MatchString(reference) {
|
||||
apiErrorDefined(ctx, errManifestInvalid.WithMessage("Tag is invalid"))
|
||||
return
|
||||
}
|
||||
@ -618,7 +621,7 @@ func getBlobSearchOptionsFromContext(ctx *context.Context) (*container_model.Blo
|
||||
reference := ctx.PathParam("reference")
|
||||
if d := digest.Digest(reference); d.Validate() == nil {
|
||||
opts.Digest = string(d)
|
||||
} else if referencePattern.MatchString(reference) {
|
||||
} else if globalVars().referencePattern.MatchString(reference) {
|
||||
opts.Tag = reference
|
||||
opts.OnlyLead = true
|
||||
} else {
|
||||
@ -782,7 +785,8 @@ func GetTagsList(ctx *context.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// FIXME: Workaround to be removed in v1.20
|
||||
// FIXME: Workaround to be removed in v1.20.
|
||||
// Update maybe we should never really remote it, as long as there is legacy data?
|
||||
// https://github.com/go-gitea/gitea/issues/19586
|
||||
func workaroundGetContainerBlob(ctx *context.Context, opts *container_model.BlobSearchOptions) (*packages_model.PackageFileDescriptor, error) {
|
||||
blob, err := container_model.GetContainerBlob(ctx, opts)
|
||||
|
@ -46,11 +46,9 @@ func processManifest(ctx context.Context, mci *manifestCreationInfo, buf *packag
|
||||
if err := json.NewDecoder(buf).Decode(&index); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if index.SchemaVersion != 2 {
|
||||
return "", errUnsupported.WithMessage("Schema version is not supported")
|
||||
}
|
||||
|
||||
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -77,24 +75,41 @@ func processManifest(ctx context.Context, mci *manifestCreationInfo, buf *packag
|
||||
return "", errManifestInvalid
|
||||
}
|
||||
|
||||
func processOciImageManifest(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) {
|
||||
manifestDigest := ""
|
||||
type processManifestTxRet struct {
|
||||
pv *packages_model.PackageVersion
|
||||
pb *packages_model.PackageBlob
|
||||
created bool
|
||||
digest string
|
||||
}
|
||||
|
||||
err := func() error {
|
||||
manifest, configDescriptor, metadata, err := container_service.ParseManifestMetadata(ctx, buf, mci.Owner.ID, mci.Image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = buf.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
func handleCreateManifestResult(ctx context.Context, err error, mci *manifestCreationInfo, contentStore *packages_module.ContentStore, txRet *processManifestTxRet) (string, error) {
|
||||
if err != nil && txRet.created && txRet.pb != nil {
|
||||
if err := contentStore.Delete(packages_module.BlobHash256Key(txRet.pb.HashSHA256)); err != nil {
|
||||
log.Error("Error deleting package blob from content store: %v", err)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
pd, err := packages_model.GetPackageDescriptor(ctx, txRet.pv)
|
||||
if err != nil {
|
||||
log.Error("Error getting package descriptor: %v", err) // ignore this error
|
||||
} else {
|
||||
notify_service.PackageCreate(ctx, mci.Creator, pd)
|
||||
}
|
||||
return txRet.digest, nil
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
func processOciImageManifest(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (manifestDigest string, errRet error) {
|
||||
manifest, configDescriptor, metadata, err := container_service.ParseManifestMetadata(ctx, buf, mci.Owner.ID, mci.Image)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err = buf.Seek(0, io.SeekStart); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
contentStore := packages_module.NewContentStore()
|
||||
var txRet processManifestTxRet
|
||||
err = db.WithTx(ctx, func(ctx context.Context) (err error) {
|
||||
blobReferences := make([]*blobReference, 0, 1+len(manifest.Layers))
|
||||
blobReferences = append(blobReferences, &blobReference{
|
||||
Digest: manifest.Config.Digest,
|
||||
@ -127,7 +142,7 @@ func processOciImageManifest(ctx context.Context, mci *manifestCreationInfo, buf
|
||||
}
|
||||
|
||||
uploadVersion, err := packages_model.GetInternalVersionByNameAndVersion(ctx, mci.Owner.ID, packages_model.TypeContainer, mci.Image, container_module.UploadVersion)
|
||||
if err != nil && err != packages_model.ErrPackageNotExist {
|
||||
if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -136,61 +151,26 @@ func processOciImageManifest(ctx context.Context, mci *manifestCreationInfo, buf
|
||||
return err
|
||||
}
|
||||
}
|
||||
txRet.pv = pv
|
||||
txRet.pb, txRet.created, txRet.digest, err = createManifestBlob(ctx, contentStore, mci, pv, buf)
|
||||
return err
|
||||
})
|
||||
|
||||
pb, created, digest, err := createManifestBlob(ctx, mci, pv, buf)
|
||||
removeBlob := false
|
||||
defer func() {
|
||||
if removeBlob {
|
||||
contentStore := packages_module.NewContentStore()
|
||||
if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
|
||||
log.Error("Error deleting package blob from content store: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
removeBlob = created
|
||||
return err
|
||||
}
|
||||
return handleCreateManifestResult(ctx, err, mci, contentStore, &txRet)
|
||||
}
|
||||
|
||||
if err := committer.Commit(); err != nil {
|
||||
removeBlob = created
|
||||
return err
|
||||
}
|
||||
|
||||
if err := notifyPackageCreate(ctx, mci.Creator, pv); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifestDigest = digest
|
||||
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
func processOciImageIndex(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (manifestDigest string, errRet error) {
|
||||
var index oci.Index
|
||||
if err := json.NewDecoder(buf).Decode(&index); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return manifestDigest, nil
|
||||
}
|
||||
|
||||
func processOciImageIndex(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) {
|
||||
manifestDigest := ""
|
||||
|
||||
err := func() error {
|
||||
var index oci.Index
|
||||
if err := json.NewDecoder(buf).Decode(&index); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
contentStore := packages_module.NewContentStore()
|
||||
var txRet processManifestTxRet
|
||||
err := db.WithTx(ctx, func(ctx context.Context) (err error) {
|
||||
metadata := &container_module.Metadata{
|
||||
Type: container_module.TypeOCI,
|
||||
Manifests: make([]*container_module.Manifest, 0, len(index.Manifests)),
|
||||
@ -241,50 +221,12 @@ func processOciImageIndex(ctx context.Context, mci *manifestCreationInfo, buf *p
|
||||
return err
|
||||
}
|
||||
|
||||
pb, created, digest, err := createManifestBlob(ctx, mci, pv, buf)
|
||||
removeBlob := false
|
||||
defer func() {
|
||||
if removeBlob {
|
||||
contentStore := packages_module.NewContentStore()
|
||||
if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil {
|
||||
log.Error("Error deleting package blob from content store: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
removeBlob = created
|
||||
return err
|
||||
}
|
||||
|
||||
if err := committer.Commit(); err != nil {
|
||||
removeBlob = created
|
||||
return err
|
||||
}
|
||||
|
||||
if err := notifyPackageCreate(ctx, mci.Creator, pv); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifestDigest = digest
|
||||
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return manifestDigest, nil
|
||||
}
|
||||
|
||||
func notifyPackageCreate(ctx context.Context, doer *user_model.User, pv *packages_model.PackageVersion) error {
|
||||
pd, err := packages_model.GetPackageDescriptor(ctx, pv)
|
||||
if err != nil {
|
||||
txRet.pv = pv
|
||||
txRet.pb, txRet.created, txRet.digest, err = createManifestBlob(ctx, contentStore, mci, pv, buf)
|
||||
return err
|
||||
}
|
||||
})
|
||||
|
||||
notify_service.PackageCreate(ctx, doer, pd)
|
||||
|
||||
return nil
|
||||
return handleCreateManifestResult(ctx, err, mci, contentStore, &txRet)
|
||||
}
|
||||
|
||||
func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, metadata *container_module.Metadata) (*packages_model.PackageVersion, error) {
|
||||
@ -437,7 +379,7 @@ func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *package
|
||||
return pf, nil
|
||||
}
|
||||
|
||||
func createManifestBlob(ctx context.Context, mci *manifestCreationInfo, pv *packages_model.PackageVersion, buf *packages_module.HashedBuffer) (*packages_model.PackageBlob, bool, string, error) {
|
||||
func createManifestBlob(ctx context.Context, contentStore *packages_module.ContentStore, mci *manifestCreationInfo, pv *packages_model.PackageVersion, buf *packages_module.HashedBuffer) (_ *packages_model.PackageBlob, created bool, manifestDigest string, _ error) {
|
||||
pb, exists, err := packages_model.GetOrInsertBlob(ctx, packages_service.NewPackageBlob(buf))
|
||||
if err != nil {
|
||||
log.Error("Error inserting package blob: %v", err)
|
||||
@ -446,21 +388,20 @@ func createManifestBlob(ctx context.Context, mci *manifestCreationInfo, pv *pack
|
||||
// FIXME: Workaround to be removed in v1.20
|
||||
// https://github.com/go-gitea/gitea/issues/19586
|
||||
if exists {
|
||||
err = packages_module.NewContentStore().Has(packages_module.BlobHash256Key(pb.HashSHA256))
|
||||
err = contentStore.Has(packages_module.BlobHash256Key(pb.HashSHA256))
|
||||
if err != nil && (errors.Is(err, util.ErrNotExist) || errors.Is(err, os.ErrNotExist)) {
|
||||
log.Debug("Package registry inconsistent: blob %s does not exist on file system", pb.HashSHA256)
|
||||
exists = false
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
contentStore := packages_module.NewContentStore()
|
||||
if err := contentStore.Save(packages_module.BlobHash256Key(pb.HashSHA256), buf, buf.Size()); err != nil {
|
||||
log.Error("Error saving package blob in content store: %v", err)
|
||||
return nil, false, "", err
|
||||
}
|
||||
}
|
||||
|
||||
manifestDigest := digestFromHashSummer(buf)
|
||||
manifestDigest = digestFromHashSummer(buf)
|
||||
pf, err := createFileFromBlobReference(ctx, pv, nil, &blobReference{
|
||||
Digest: digest.Digest(manifestDigest),
|
||||
MediaType: mci.MediaType,
|
||||
|
@ -29,7 +29,7 @@ func CreateOrg(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of the user that will own the created organization
|
||||
// description: username of the user who will own the created organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: organization
|
||||
|
@ -22,7 +22,7 @@ func CreateRepo(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of the user. This user will own the created repository
|
||||
// description: username of the user who will own the created repository
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repository
|
||||
|
@ -175,7 +175,7 @@ func EditUser(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user to edit
|
||||
// description: username of the user whose data is to be edited
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
@ -272,7 +272,7 @@ func DeleteUser(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user to delete
|
||||
// description: username of the user to delete
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: purge
|
||||
@ -328,7 +328,7 @@ func CreatePublicKey(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of the user
|
||||
// description: username of the user who is to receive a public key
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: key
|
||||
@ -358,7 +358,7 @@ func DeleteUserPublicKey(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user
|
||||
// description: username of the user whose public key is to be deleted
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
@ -405,7 +405,7 @@ func SearchUsers(ctx *context.APIContext) {
|
||||
// format: int64
|
||||
// - name: login_name
|
||||
// in: query
|
||||
// description: user's login name to search for
|
||||
// description: identifier of the user, provided by the external authenticator
|
||||
// type: string
|
||||
// - name: page
|
||||
// in: query
|
||||
@ -456,7 +456,7 @@ func RenameUser(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: existing username of user
|
||||
// description: current username of the user
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
|
@ -22,7 +22,7 @@ func ListUserBadges(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user
|
||||
// description: username of the user whose badges are to be listed
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
@ -53,7 +53,7 @@ func AddUserBadges(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user
|
||||
// description: username of the user to whom a badge is to be added
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
@ -87,7 +87,7 @@ func DeleteUserBadges(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user
|
||||
// description: username of the user whose badge is to be deleted
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
|
@ -47,7 +47,7 @@ func CheckUserBlock(ctx *context.APIContext) {
|
||||
// required: true
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: user to check
|
||||
// description: username of the user to check
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
@ -71,7 +71,7 @@ func BlockUser(ctx *context.APIContext) {
|
||||
// required: true
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: user to block
|
||||
// description: username of the user to block
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: note
|
||||
@ -101,7 +101,7 @@ func UnblockUser(ctx *context.APIContext) {
|
||||
// required: true
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: user to unblock
|
||||
// description: username of the user to unblock
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
|
@ -133,7 +133,7 @@ func IsMember(ctx *context.APIContext) {
|
||||
// required: true
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of the user
|
||||
// description: username of the user to check for an organization membership
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
@ -186,7 +186,7 @@ func IsPublicMember(ctx *context.APIContext) {
|
||||
// required: true
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of the user
|
||||
// description: username of the user to check for a public organization membership
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
@ -240,7 +240,7 @@ func PublicizeMember(ctx *context.APIContext) {
|
||||
// required: true
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of the user
|
||||
// description: username of the user whose membership is to be publicized
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
@ -282,7 +282,7 @@ func ConcealMember(ctx *context.APIContext) {
|
||||
// required: true
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of the user
|
||||
// description: username of the user whose membership is to be concealed
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
@ -324,7 +324,7 @@ func DeleteMember(ctx *context.APIContext) {
|
||||
// required: true
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of the user
|
||||
// description: username of the user to remove from the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
|
@ -82,7 +82,7 @@ func ListUserOrgs(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user
|
||||
// description: username of the user whose organizations are to be listed
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
@ -112,7 +112,7 @@ func GetUserOrgsPermissions(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user
|
||||
// description: username of the user whose permissions are to be obtained
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: org
|
||||
|
@ -426,7 +426,7 @@ func GetTeamMember(ctx *context.APIContext) {
|
||||
// required: true
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of the member to list
|
||||
// description: username of the user whose data is to be listed
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
@ -467,7 +467,7 @@ func AddTeamMember(ctx *context.APIContext) {
|
||||
// required: true
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of the user to add
|
||||
// description: username of the user to add to a team
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
@ -509,7 +509,7 @@ func RemoveTeamMember(ctx *context.APIContext) {
|
||||
// required: true
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of the user to remove
|
||||
// description: username of the user to remove from a team
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
|
@ -93,7 +93,7 @@ func IsCollaborator(ctx *context.APIContext) {
|
||||
// required: true
|
||||
// - name: collaborator
|
||||
// in: path
|
||||
// description: username of the collaborator
|
||||
// description: username of the user to check for being a collaborator
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
@ -145,7 +145,7 @@ func AddOrUpdateCollaborator(ctx *context.APIContext) {
|
||||
// required: true
|
||||
// - name: collaborator
|
||||
// in: path
|
||||
// description: username of the collaborator to add
|
||||
// description: username of the user to add or update as a collaborator
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
@ -264,7 +264,7 @@ func GetRepoPermissions(ctx *context.APIContext) {
|
||||
// required: true
|
||||
// - name: collaborator
|
||||
// in: path
|
||||
// description: username of the collaborator
|
||||
// description: username of the collaborator whose permissions are to be obtained
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
|
@ -43,7 +43,7 @@ func AddIssueSubscription(ctx *context.APIContext) {
|
||||
// required: true
|
||||
// - name: user
|
||||
// in: path
|
||||
// description: user to subscribe
|
||||
// description: username of the user to subscribe the issue to
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
@ -87,7 +87,7 @@ func DelIssueSubscription(ctx *context.APIContext) {
|
||||
// required: true
|
||||
// - name: user
|
||||
// in: path
|
||||
// description: user witch unsubscribe
|
||||
// description: username of the user to unsubscribe from an issue
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
|
@ -405,7 +405,7 @@ func ListTrackedTimesByUser(ctx *context.APIContext) {
|
||||
// required: true
|
||||
// - name: user
|
||||
// in: path
|
||||
// description: username of user
|
||||
// description: username of the user whose tracked times are to be listed
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
|
@ -30,7 +30,7 @@ func ListAccessTokens(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user
|
||||
// description: username of to user whose access tokens are to be listed
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
@ -83,7 +83,7 @@ func CreateAccessToken(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user
|
||||
// description: username of the user whose token is to be created
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: body
|
||||
@ -149,7 +149,7 @@ func DeleteAccessToken(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user
|
||||
// description: username of the user whose token is to be deleted
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: token
|
||||
|
@ -37,7 +37,7 @@ func CheckUserBlock(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: user to check
|
||||
// description: username of the user to check
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
@ -56,7 +56,7 @@ func BlockUser(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: user to block
|
||||
// description: username of the user to block
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: note
|
||||
@ -81,7 +81,7 @@ func UnblockUser(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: user to unblock
|
||||
// description: username of the user to unblock
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
|
@ -67,7 +67,7 @@ func ListFollowers(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user
|
||||
// description: username of the user whose followers are to be listed
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
@ -131,7 +131,7 @@ func ListFollowing(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user
|
||||
// description: username of the user whose followed users are to be listed
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
@ -167,7 +167,7 @@ func CheckMyFollowing(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of followed user
|
||||
// description: username of the user to check for authenticated followers
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
@ -187,12 +187,12 @@ func CheckFollowing(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of following user
|
||||
// description: username of the following user
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: target
|
||||
// in: path
|
||||
// description: username of followed user
|
||||
// description: username of the followed user
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
@ -216,7 +216,7 @@ func Follow(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user to follow
|
||||
// description: username of the user to follow
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
@ -246,7 +246,7 @@ func Unfollow(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user to unfollow
|
||||
// description: username of the user to unfollow
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
|
@ -53,7 +53,7 @@ func ListGPGKeys(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user
|
||||
// description: username of the user whose GPG key list is to be obtained
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
|
@ -136,7 +136,7 @@ func ListPublicKeys(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user
|
||||
// description: username of the user whose public keys are to be listed
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: fingerprint
|
||||
|
@ -62,7 +62,7 @@ func ListUserRepos(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user
|
||||
// description: username of the user whose owned repos are to be listed
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
|
@ -50,7 +50,7 @@ func GetStarredRepos(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user
|
||||
// description: username of the user whose starred repos are to be listed
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
|
@ -110,7 +110,7 @@ func GetInfo(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user to get
|
||||
// description: username of the user whose data is to be listed
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
@ -151,7 +151,7 @@ func GetUserHeatmapData(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user to get
|
||||
// description: username of the user whose heatmap is to be obtained
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
@ -177,7 +177,7 @@ func ListUserActivityFeeds(ctx *context.APIContext) {
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user
|
||||
// description: username of the user whose activity feeds are to be listed
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: only-performed-by
|
||||
|
@ -49,7 +49,7 @@ func GetWatchedRepos(ctx *context.APIContext) {
|
||||
// - name: username
|
||||
// type: string
|
||||
// in: path
|
||||
// description: username of the user
|
||||
// description: username of the user whose watched repos are to be listed
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
|
@ -244,7 +244,7 @@ func editFileOpenExisting(ctx *context.Context) (prefetch []byte, dataRc io.Read
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
if fInfo.isLFSFile {
|
||||
if fInfo.isLFSFile() {
|
||||
lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath)
|
||||
if err != nil {
|
||||
_ = dataRc.Close()
|
||||
@ -298,7 +298,7 @@ func EditFile(ctx *context.Context) {
|
||||
ctx.Data["FileSize"] = fInfo.fileSize
|
||||
|
||||
// Only some file types are editable online as text.
|
||||
if fInfo.isLFSFile {
|
||||
if fInfo.isLFSFile() {
|
||||
ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_lfs_files")
|
||||
} else if !fInfo.st.IsRepresentableAsText() {
|
||||
ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_non_text_files")
|
||||
|
@ -267,8 +267,10 @@ func LFSFileGet(ctx *context.Context) {
|
||||
buf = buf[:n]
|
||||
|
||||
st := typesniffer.DetectContentType(buf)
|
||||
// FIXME: there is no IsPlainText set, but template uses it
|
||||
ctx.Data["IsTextFile"] = st.IsText()
|
||||
ctx.Data["FileSize"] = meta.Size
|
||||
// FIXME: the last field is the URL-base64-encoded filename, it should not be "direct"
|
||||
ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s/%s.git/info/lfs/objects/%s/%s", setting.AppURL, url.PathEscape(ctx.Repo.Repository.OwnerName), url.PathEscape(ctx.Repo.Repository.Name), url.PathEscape(meta.Oid), "direct")
|
||||
switch {
|
||||
case st.IsRepresentableAsText():
|
||||
@ -309,8 +311,6 @@ func LFSFileGet(ctx *context.Context) {
|
||||
}
|
||||
ctx.Data["LineNums"] = gotemplate.HTML(output.String())
|
||||
|
||||
case st.IsPDF():
|
||||
ctx.Data["IsPDFFile"] = true
|
||||
case st.IsVideo():
|
||||
ctx.Data["IsVideoFile"] = true
|
||||
case st.IsAudio():
|
||||
|
@ -59,60 +59,63 @@ const (
|
||||
)
|
||||
|
||||
type fileInfo struct {
|
||||
isTextFile bool
|
||||
isLFSFile bool
|
||||
fileSize int64
|
||||
lfsMeta *lfs.Pointer
|
||||
st typesniffer.SniffedType
|
||||
fileSize int64
|
||||
lfsMeta *lfs.Pointer
|
||||
st typesniffer.SniffedType
|
||||
}
|
||||
|
||||
func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte, io.ReadCloser, *fileInfo, error) {
|
||||
dataRc, err := blob.DataAsync()
|
||||
func (fi *fileInfo) isLFSFile() bool {
|
||||
return fi.lfsMeta != nil && fi.lfsMeta.Oid != ""
|
||||
}
|
||||
|
||||
func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) (buf []byte, dataRc io.ReadCloser, fi *fileInfo, err error) {
|
||||
dataRc, err = blob.DataAsync()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
const prefetchSize = lfs.MetaFileMaxSize
|
||||
|
||||
buf = make([]byte, prefetchSize)
|
||||
n, _ := util.ReadAtMost(dataRc, buf)
|
||||
buf = buf[:n]
|
||||
|
||||
st := typesniffer.DetectContentType(buf)
|
||||
isTextFile := st.IsText()
|
||||
fi = &fileInfo{fileSize: blob.Size(), st: typesniffer.DetectContentType(buf)}
|
||||
|
||||
// FIXME: what happens when README file is an image?
|
||||
if !isTextFile || !setting.LFS.StartServer {
|
||||
return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
|
||||
if !fi.st.IsText() || !setting.LFS.StartServer {
|
||||
return buf, dataRc, fi, nil
|
||||
}
|
||||
|
||||
pointer, _ := lfs.ReadPointerFromBuffer(buf)
|
||||
if !pointer.IsValid() { // fallback to plain file
|
||||
return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
|
||||
if !pointer.IsValid() { // fallback to a plain file
|
||||
return buf, dataRc, fi, nil
|
||||
}
|
||||
|
||||
meta, err := git_model.GetLFSMetaObjectByOid(ctx, repoID, pointer.Oid)
|
||||
if err != nil { // fallback to plain file
|
||||
if err != nil { // fallback to a plain file
|
||||
log.Warn("Unable to access LFS pointer %s in repo %d: %v", pointer.Oid, repoID, err)
|
||||
return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil
|
||||
return buf, dataRc, fi, nil
|
||||
}
|
||||
|
||||
dataRc.Close()
|
||||
|
||||
// close the old dataRc and open the real LFS target
|
||||
_ = dataRc.Close()
|
||||
dataRc, err = lfs.ReadMetaObject(pointer)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
buf = make([]byte, 1024)
|
||||
buf = make([]byte, prefetchSize)
|
||||
n, err = util.ReadAtMost(dataRc, buf)
|
||||
if err != nil {
|
||||
dataRc.Close()
|
||||
return nil, nil, nil, err
|
||||
_ = dataRc.Close()
|
||||
return nil, nil, fi, err
|
||||
}
|
||||
buf = buf[:n]
|
||||
|
||||
st = typesniffer.DetectContentType(buf)
|
||||
|
||||
return buf, dataRc, &fileInfo{st.IsText(), true, meta.Size, &meta.Pointer, st}, nil
|
||||
fi.st = typesniffer.DetectContentType(buf)
|
||||
fi.fileSize = blob.Size()
|
||||
fi.lfsMeta = &meta.Pointer
|
||||
return buf, dataRc, fi, nil
|
||||
}
|
||||
|
||||
func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool {
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/typesniffer"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
@ -40,7 +41,128 @@ func prepareLatestCommitInfo(ctx *context.Context) bool {
|
||||
return loadLatestCommitData(ctx, commit)
|
||||
}
|
||||
|
||||
func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||
func prepareFileViewLfsAttrs(ctx *context.Context) (*attribute.Attributes, bool) {
|
||||
attrsMap, err := attribute.CheckAttributes(ctx, ctx.Repo.GitRepo, ctx.Repo.CommitID, attribute.CheckAttributeOpts{
|
||||
Filenames: []string{ctx.Repo.TreePath},
|
||||
Attributes: []string{attribute.LinguistGenerated, attribute.LinguistVendored, attribute.LinguistLanguage, attribute.GitlabLanguage},
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("attribute.CheckAttributes", err)
|
||||
return nil, false
|
||||
}
|
||||
attrs := attrsMap[ctx.Repo.TreePath]
|
||||
if attrs == nil {
|
||||
// this case shouldn't happen, just in case.
|
||||
setting.PanicInDevOrTesting("no attributes found for %s", ctx.Repo.TreePath)
|
||||
attrs = attribute.NewAttributes()
|
||||
}
|
||||
ctx.Data["IsVendored"], ctx.Data["IsGenerated"] = attrs.GetVendored().Value(), attrs.GetGenerated().Value()
|
||||
return attrs, true
|
||||
}
|
||||
|
||||
func handleFileViewRenderMarkup(ctx *context.Context, filename string, sniffedType typesniffer.SniffedType, prefetchBuf []byte, utf8Reader io.Reader) bool {
|
||||
markupType := markup.DetectMarkupTypeByFileName(filename)
|
||||
if markupType == "" {
|
||||
markupType = markup.DetectRendererType(filename, sniffedType, prefetchBuf)
|
||||
}
|
||||
if markupType == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
ctx.Data["HasSourceRenderedToggle"] = true
|
||||
|
||||
if ctx.FormString("display") == "source" {
|
||||
return false
|
||||
}
|
||||
|
||||
ctx.Data["MarkupType"] = markupType
|
||||
metas := ctx.Repo.Repository.ComposeRepoFileMetas(ctx)
|
||||
metas["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL()
|
||||
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
|
||||
CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
|
||||
CurrentTreePath: path.Dir(ctx.Repo.TreePath),
|
||||
}).
|
||||
WithMarkupType(markupType).
|
||||
WithRelativePath(ctx.Repo.TreePath).
|
||||
WithMetas(metas)
|
||||
|
||||
var err error
|
||||
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, utf8Reader)
|
||||
if err != nil {
|
||||
ctx.ServerError("Render", err)
|
||||
return true
|
||||
}
|
||||
// to prevent iframe from loading third-party url
|
||||
ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'")
|
||||
return true
|
||||
}
|
||||
|
||||
func handleFileViewRenderSource(ctx *context.Context, filename string, attrs *attribute.Attributes, fInfo *fileInfo, utf8Reader io.Reader) bool {
|
||||
if ctx.FormString("display") == "rendered" || !fInfo.st.IsRepresentableAsText() {
|
||||
return false
|
||||
}
|
||||
|
||||
if !fInfo.st.IsText() {
|
||||
if ctx.FormString("display") == "" {
|
||||
// not text but representable as text, e.g. SVG
|
||||
// since there is no "display" is specified, let other renders to handle
|
||||
return false
|
||||
}
|
||||
ctx.Data["HasSourceRenderedToggle"] = true
|
||||
}
|
||||
|
||||
buf, _ := io.ReadAll(utf8Reader)
|
||||
// The Open Group Base Specification: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html
|
||||
// empty: 0 lines; "a": 1 incomplete-line; "a\n": 1 line; "a\nb": 1 line, 1 incomplete-line;
|
||||
// Gitea uses the definition (like most modern editors):
|
||||
// empty: 0 lines; "a": 1 line; "a\n": 2 lines; "a\nb": 2 lines;
|
||||
// When rendering, the last empty line is not rendered in UI, while the line-number is still counted, to tell users that the file contains a trailing EOL.
|
||||
// To make the UI more consistent, it could use an icon mark to indicate that there is no trailing EOL, and show line-number as the rendered lines.
|
||||
// This NumLines is only used for the display on the UI: "xxx lines"
|
||||
if len(buf) == 0 {
|
||||
ctx.Data["NumLines"] = 0
|
||||
} else {
|
||||
ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1
|
||||
}
|
||||
|
||||
language := attrs.GetLanguage().Value()
|
||||
fileContent, lexerName, err := highlight.File(filename, language, buf)
|
||||
ctx.Data["LexerName"] = lexerName
|
||||
if err != nil {
|
||||
log.Error("highlight.File failed, fallback to plain text: %v", err)
|
||||
fileContent = highlight.PlainText(buf)
|
||||
}
|
||||
status := &charset.EscapeStatus{}
|
||||
statuses := make([]*charset.EscapeStatus, len(fileContent))
|
||||
for i, line := range fileContent {
|
||||
statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale)
|
||||
status = status.Or(statuses[i])
|
||||
}
|
||||
ctx.Data["EscapeStatus"] = status
|
||||
ctx.Data["FileContent"] = fileContent
|
||||
ctx.Data["LineEscapeStatus"] = statuses
|
||||
return true
|
||||
}
|
||||
|
||||
func handleFileViewRenderImage(ctx *context.Context, fInfo *fileInfo, prefetchBuf []byte) bool {
|
||||
if !fInfo.st.IsImage() {
|
||||
return false
|
||||
}
|
||||
if fInfo.st.IsSvgImage() && !setting.UI.SVG.Enabled {
|
||||
return false
|
||||
}
|
||||
if fInfo.st.IsSvgImage() {
|
||||
ctx.Data["HasSourceRenderedToggle"] = true
|
||||
} else {
|
||||
img, _, err := image.DecodeConfig(bytes.NewReader(prefetchBuf))
|
||||
if err == nil { // ignore the error for the formats that are not supported by image.DecodeConfig
|
||||
ctx.Data["ImageSize"] = fmt.Sprintf("%dx%dpx", img.Width, img.Height)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func prepareFileView(ctx *context.Context, entry *git.TreeEntry) {
|
||||
ctx.Data["IsViewFile"] = true
|
||||
ctx.Data["HideRepoInfo"] = true
|
||||
|
||||
@ -86,11 +208,8 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||
}
|
||||
}
|
||||
|
||||
isDisplayingSource := ctx.FormString("display") == "source"
|
||||
isDisplayingRendered := !isDisplayingSource
|
||||
|
||||
// Don't call any other repository functions depends on git.Repository until the dataRc closed to
|
||||
// avoid create unnecessary temporary cat file.
|
||||
// avoid creating an unnecessary temporary cat file.
|
||||
buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob)
|
||||
if err != nil {
|
||||
ctx.ServerError("getFileReader", err)
|
||||
@ -98,207 +217,62 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||
}
|
||||
defer dataRc.Close()
|
||||
|
||||
if fInfo.isLFSFile {
|
||||
if fInfo.isLFSFile() {
|
||||
ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
|
||||
}
|
||||
|
||||
isRepresentableAsText := fInfo.st.IsRepresentableAsText()
|
||||
if !isRepresentableAsText {
|
||||
// If we can't show plain text, always try to render.
|
||||
isDisplayingSource = false
|
||||
isDisplayingRendered = true
|
||||
if !prepareFileViewEditorButtons(ctx) {
|
||||
return
|
||||
}
|
||||
ctx.Data["IsLFSFile"] = fInfo.isLFSFile
|
||||
|
||||
ctx.Data["IsLFSFile"] = fInfo.isLFSFile()
|
||||
ctx.Data["FileSize"] = fInfo.fileSize
|
||||
ctx.Data["IsTextFile"] = fInfo.isTextFile
|
||||
ctx.Data["IsRepresentableAsText"] = isRepresentableAsText
|
||||
ctx.Data["IsDisplayingSource"] = isDisplayingSource
|
||||
ctx.Data["IsDisplayingRendered"] = isDisplayingRendered
|
||||
ctx.Data["IsRepresentableAsText"] = fInfo.st.IsRepresentableAsText()
|
||||
ctx.Data["IsExecutable"] = entry.IsExecutable()
|
||||
ctx.Data["CanCopyContent"] = fInfo.st.IsRepresentableAsText() || fInfo.st.IsImage()
|
||||
|
||||
isTextSource := fInfo.isTextFile || isDisplayingSource
|
||||
ctx.Data["IsTextSource"] = isTextSource
|
||||
if isTextSource {
|
||||
ctx.Data["CanCopyContent"] = true
|
||||
}
|
||||
|
||||
// Check LFS Lock
|
||||
lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath)
|
||||
ctx.Data["LFSLock"] = lfsLock
|
||||
if err != nil {
|
||||
ctx.ServerError("GetTreePathLock", err)
|
||||
attrs, ok := prepareFileViewLfsAttrs(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if lfsLock != nil {
|
||||
u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetTreePathLock", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["LFSLockOwner"] = u.Name
|
||||
ctx.Data["LFSLockOwnerHomeLink"] = u.HomeLink()
|
||||
ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked")
|
||||
}
|
||||
|
||||
// read all needed attributes which will be used later
|
||||
// there should be no performance different between reading 2 or 4 here
|
||||
attrsMap, err := attribute.CheckAttributes(ctx, ctx.Repo.GitRepo, ctx.Repo.CommitID, attribute.CheckAttributeOpts{
|
||||
Filenames: []string{ctx.Repo.TreePath},
|
||||
Attributes: []string{attribute.LinguistGenerated, attribute.LinguistVendored, attribute.LinguistLanguage, attribute.GitlabLanguage},
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("attribute.CheckAttributes", err)
|
||||
return
|
||||
}
|
||||
attrs := attrsMap[ctx.Repo.TreePath]
|
||||
if attrs == nil {
|
||||
// this case shouldn't happen, just in case.
|
||||
setting.PanicInDevOrTesting("no attributes found for %s", ctx.Repo.TreePath)
|
||||
attrs = attribute.NewAttributes()
|
||||
}
|
||||
// TODO: in the future maybe we need more accurate flags, for example:
|
||||
// * IsRepresentableAsText: some files are text, some are not
|
||||
// * IsRenderableXxx: some files are rendered by backend "markup" engine, some are rendered by frontend (pdf, 3d)
|
||||
// * DefaultViewMode: when there is no "display" query parameter, which view mode should be used by default, source or rendered
|
||||
|
||||
utf8Reader := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
|
||||
switch {
|
||||
case isRepresentableAsText:
|
||||
if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
|
||||
ctx.Data["IsFileTooLarge"] = true
|
||||
break
|
||||
}
|
||||
|
||||
if fInfo.st.IsSvgImage() {
|
||||
ctx.Data["IsImageFile"] = true
|
||||
ctx.Data["CanCopyContent"] = true
|
||||
ctx.Data["HasSourceRenderedToggle"] = true
|
||||
}
|
||||
|
||||
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
|
||||
|
||||
shouldRenderSource := ctx.FormString("display") == "source"
|
||||
readmeExist := util.IsReadmeFileName(blob.Name())
|
||||
ctx.Data["ReadmeExist"] = readmeExist
|
||||
|
||||
markupType := markup.DetectMarkupTypeByFileName(blob.Name())
|
||||
if markupType == "" {
|
||||
markupType = markup.DetectRendererType(blob.Name(), bytes.NewReader(buf))
|
||||
}
|
||||
if markupType != "" {
|
||||
ctx.Data["HasSourceRenderedToggle"] = true
|
||||
}
|
||||
if markupType != "" && !shouldRenderSource {
|
||||
ctx.Data["IsMarkup"] = true
|
||||
ctx.Data["MarkupType"] = markupType
|
||||
metas := ctx.Repo.Repository.ComposeRepoFileMetas(ctx)
|
||||
metas["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL()
|
||||
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
|
||||
CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
|
||||
CurrentTreePath: path.Dir(ctx.Repo.TreePath),
|
||||
}).
|
||||
WithMarkupType(markupType).
|
||||
WithRelativePath(ctx.Repo.TreePath).
|
||||
WithMetas(metas)
|
||||
|
||||
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd)
|
||||
if err != nil {
|
||||
ctx.ServerError("Render", err)
|
||||
return
|
||||
}
|
||||
// to prevent iframe load third-party url
|
||||
ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'")
|
||||
} else {
|
||||
buf, _ := io.ReadAll(rd)
|
||||
|
||||
// The Open Group Base Specification: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html
|
||||
// empty: 0 lines; "a": 1 incomplete-line; "a\n": 1 line; "a\nb": 1 line, 1 incomplete-line;
|
||||
// Gitea uses the definition (like most modern editors):
|
||||
// empty: 0 lines; "a": 1 line; "a\n": 2 lines; "a\nb": 2 lines;
|
||||
// When rendering, the last empty line is not rendered in UI, while the line-number is still counted, to tell users that the file contains a trailing EOL.
|
||||
// To make the UI more consistent, it could use an icon mark to indicate that there is no trailing EOL, and show line-number as the rendered lines.
|
||||
// This NumLines is only used for the display on the UI: "xxx lines"
|
||||
if len(buf) == 0 {
|
||||
ctx.Data["NumLines"] = 0
|
||||
} else {
|
||||
ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1
|
||||
}
|
||||
|
||||
language := attrs.GetLanguage().Value()
|
||||
fileContent, lexerName, err := highlight.File(blob.Name(), language, buf)
|
||||
ctx.Data["LexerName"] = lexerName
|
||||
if err != nil {
|
||||
log.Error("highlight.File failed, fallback to plain text: %v", err)
|
||||
fileContent = highlight.PlainText(buf)
|
||||
}
|
||||
status := &charset.EscapeStatus{}
|
||||
statuses := make([]*charset.EscapeStatus, len(fileContent))
|
||||
for i, line := range fileContent {
|
||||
statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale)
|
||||
status = status.Or(statuses[i])
|
||||
}
|
||||
ctx.Data["EscapeStatus"] = status
|
||||
ctx.Data["FileContent"] = fileContent
|
||||
ctx.Data["LineEscapeStatus"] = statuses
|
||||
}
|
||||
|
||||
case fInfo.st.IsPDF():
|
||||
ctx.Data["IsPDFFile"] = true
|
||||
case fInfo.fileSize >= setting.UI.MaxDisplayFileSize:
|
||||
ctx.Data["IsFileTooLarge"] = true
|
||||
case handleFileViewRenderMarkup(ctx, entry.Name(), fInfo.st, buf, utf8Reader):
|
||||
// it also sets ctx.Data["FileContent"] and more
|
||||
ctx.Data["IsMarkup"] = true
|
||||
case handleFileViewRenderSource(ctx, entry.Name(), attrs, fInfo, utf8Reader):
|
||||
// it also sets ctx.Data["FileContent"] and more
|
||||
ctx.Data["IsDisplayingSource"] = true
|
||||
case handleFileViewRenderImage(ctx, fInfo, buf):
|
||||
ctx.Data["IsImageFile"] = true
|
||||
case fInfo.st.IsVideo():
|
||||
ctx.Data["IsVideoFile"] = true
|
||||
case fInfo.st.IsAudio():
|
||||
ctx.Data["IsAudioFile"] = true
|
||||
case fInfo.st.IsImage() && (setting.UI.SVG.Enabled || !fInfo.st.IsSvgImage()):
|
||||
ctx.Data["IsImageFile"] = true
|
||||
ctx.Data["CanCopyContent"] = true
|
||||
default:
|
||||
if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
|
||||
ctx.Data["IsFileTooLarge"] = true
|
||||
break
|
||||
}
|
||||
|
||||
// TODO: this logic duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go"
|
||||
// It is used by "external renders", markupRender will execute external programs to get rendered content.
|
||||
if markupType := markup.DetectMarkupTypeByFileName(blob.Name()); markupType != "" {
|
||||
rd := io.MultiReader(bytes.NewReader(buf), dataRc)
|
||||
ctx.Data["IsMarkup"] = true
|
||||
ctx.Data["MarkupType"] = markupType
|
||||
|
||||
rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{
|
||||
CurrentRefPath: ctx.Repo.RefTypeNameSubURL(),
|
||||
CurrentTreePath: path.Dir(ctx.Repo.TreePath),
|
||||
}).
|
||||
WithMarkupType(markupType).
|
||||
WithRelativePath(ctx.Repo.TreePath)
|
||||
|
||||
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd)
|
||||
if err != nil {
|
||||
ctx.ServerError("Render", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
// unable to render anything, show the "view raw" or let frontend handle it
|
||||
}
|
||||
|
||||
ctx.Data["IsVendored"], ctx.Data["IsGenerated"] = attrs.GetVendored().Value(), attrs.GetGenerated().Value()
|
||||
|
||||
if fInfo.st.IsImage() && !fInfo.st.IsSvgImage() {
|
||||
img, _, err := image.DecodeConfig(bytes.NewReader(buf))
|
||||
if err == nil {
|
||||
// There are Image formats go can't decode
|
||||
// Instead of throwing an error in that case, we show the size only when we can decode
|
||||
ctx.Data["ImageSize"] = fmt.Sprintf("%dx%dpx", img.Width, img.Height)
|
||||
}
|
||||
}
|
||||
|
||||
prepareToRenderButtons(ctx, lfsLock)
|
||||
}
|
||||
|
||||
func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) {
|
||||
func prepareFileViewEditorButtons(ctx *context.Context) bool {
|
||||
// archived or mirror repository, the buttons should not be shown
|
||||
if !ctx.Repo.Repository.CanEnableEditor() {
|
||||
return
|
||||
return true
|
||||
}
|
||||
|
||||
// The buttons should not be shown if it's not a branch
|
||||
if !ctx.Repo.RefFullName.IsBranch() {
|
||||
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
|
||||
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch")
|
||||
return
|
||||
return true
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) {
|
||||
@ -306,7 +280,24 @@ func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) {
|
||||
ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit")
|
||||
ctx.Data["CanDeleteFile"] = true
|
||||
ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access")
|
||||
return
|
||||
return true
|
||||
}
|
||||
|
||||
lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath)
|
||||
ctx.Data["LFSLock"] = lfsLock
|
||||
if err != nil {
|
||||
ctx.ServerError("GetTreePathLock", err)
|
||||
return false
|
||||
}
|
||||
if lfsLock != nil {
|
||||
u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetTreePathLock", err)
|
||||
return false
|
||||
}
|
||||
ctx.Data["LFSLockOwner"] = u.Name
|
||||
ctx.Data["LFSLockOwnerHomeLink"] = u.HomeLink()
|
||||
ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked")
|
||||
}
|
||||
|
||||
// it's a lfs file and the user is not the owner of the lock
|
||||
@ -315,4 +306,5 @@ func prepareToRenderButtons(ctx *context.Context, lfsLock *git_model.LFSLock) {
|
||||
ctx.Data["EditFileTooltip"] = util.Iif(isLFSLocked, ctx.Tr("repo.editor.this_file_locked"), ctx.Tr("repo.editor.edit_this_file"))
|
||||
ctx.Data["CanDeleteFile"] = !isLFSLocked
|
||||
ctx.Data["DeleteFileTooltip"] = util.Iif(isLFSLocked, ctx.Tr("repo.editor.this_file_locked"), ctx.Tr("repo.editor.delete_this_file"))
|
||||
return true
|
||||
}
|
||||
|
@ -339,7 +339,7 @@ func prepareToRenderDirOrFile(entry *git.TreeEntry) func(ctx *context.Context) {
|
||||
if entry.IsDir() {
|
||||
prepareToRenderDirectory(ctx)
|
||||
} else {
|
||||
prepareToRenderFile(ctx, entry)
|
||||
prepareFileView(ctx, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -161,24 +161,23 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil
|
||||
}
|
||||
defer dataRc.Close()
|
||||
|
||||
ctx.Data["FileIsText"] = fInfo.isTextFile
|
||||
ctx.Data["FileIsText"] = fInfo.st.IsText()
|
||||
ctx.Data["FileTreePath"] = path.Join(ctx.Repo.TreePath, subfolder, readmeFile.Name())
|
||||
ctx.Data["FileSize"] = fInfo.fileSize
|
||||
ctx.Data["IsLFSFile"] = fInfo.isLFSFile
|
||||
ctx.Data["IsLFSFile"] = fInfo.isLFSFile()
|
||||
|
||||
if fInfo.isLFSFile {
|
||||
if fInfo.isLFSFile() {
|
||||
filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.Name()))
|
||||
ctx.Data["RawFileLink"] = fmt.Sprintf("%s.git/info/lfs/objects/%s/%s", ctx.Repo.Repository.Link(), url.PathEscape(fInfo.lfsMeta.Oid), url.PathEscape(filenameBase64))
|
||||
}
|
||||
|
||||
if !fInfo.isTextFile {
|
||||
if !fInfo.st.IsText() {
|
||||
return
|
||||
}
|
||||
|
||||
if fInfo.fileSize >= setting.UI.MaxDisplayFileSize {
|
||||
// Pretend that this is a normal text file to display 'This file is too large to be shown'
|
||||
ctx.Data["IsFileTooLarge"] = true
|
||||
ctx.Data["IsTextFile"] = true
|
||||
return
|
||||
}
|
||||
|
||||
@ -212,7 +211,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil
|
||||
ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale)
|
||||
}
|
||||
|
||||
if !fInfo.isLFSFile && ctx.Repo.Repository.CanEnableEditor() {
|
||||
if !fInfo.isLFSFile() && ctx.Repo.Repository.CanEnableEditor() {
|
||||
ctx.Data["CanEditReadmeFile"] = true
|
||||
}
|
||||
}
|
||||
|
@ -203,9 +203,6 @@ func ViewPackageVersion(ctx *context.Context) {
|
||||
}
|
||||
ctx.Data["PackageRegistryHost"] = registryHostURL.Host
|
||||
|
||||
var pvs []*packages_model.PackageVersion
|
||||
pvsTotal := int64(0)
|
||||
|
||||
switch pd.Package.Type {
|
||||
case packages_model.TypeAlpine:
|
||||
branches := make(container.Set[string])
|
||||
@ -296,12 +293,16 @@ func ViewPackageVersion(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
ctx.Data["ContainerImageMetadata"] = imageMetadata
|
||||
}
|
||||
var pvs []*packages_model.PackageVersion
|
||||
var pvsTotal int64
|
||||
if pd.Package.Type == packages_model.TypeContainer {
|
||||
pvs, pvsTotal, err = container_model.SearchImageTags(ctx, &container_model.ImageTagsSearchOptions{
|
||||
Paginator: db.NewAbsoluteListOptions(0, 5),
|
||||
PackageID: pd.Package.ID,
|
||||
IsTagged: true,
|
||||
})
|
||||
default:
|
||||
} else {
|
||||
pvs, pvsTotal, err = packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
Paginator: db.NewAbsoluteListOptions(0, 5),
|
||||
PackageID: pd.Package.ID,
|
||||
@ -312,7 +313,6 @@ func ViewPackageVersion(ctx *context.Context) {
|
||||
ctx.ServerError("", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["LatestVersions"] = pvs
|
||||
ctx.Data["TotalVersionCount"] = pvsTotal
|
||||
|
||||
|
@ -48,7 +48,7 @@ func ParseManifestMetadata(ctx context.Context, rd io.Reader, ownerID int64, ima
|
||||
configDescriptor, err := container_service.GetContainerBlob(ctx, &container_service.BlobSearchOptions{
|
||||
OwnerID: ownerID,
|
||||
Image: imageName,
|
||||
Digest: string(manifest.Config.Digest),
|
||||
Digest: manifest.Config.Digest.String(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
|
@ -82,6 +82,8 @@
|
||||
</table>
|
||||
{{end}}{{/* end if .IsFileTooLarge */}}
|
||||
<div class="code-line-menu tippy-target">
|
||||
{{/*FIXME: the "HasSourceRenderedToggle" is never set on blame page, it should mean "whether the file is renderable".
|
||||
If the file is renderable, then it must has the "display=source" parameter to make sure the file view page shows the source code, then line number works. */}}
|
||||
{{if $.Permission.CanRead ctx.Consts.RepoUnitTypeIssues}}
|
||||
<a class="item ref-in-new-issue" role="menuitem" data-url-issue-new="{{.RepoLink}}/issues/new" data-url-param-body-link="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}" rel="nofollow noindex">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</a>
|
||||
{{end}}
|
||||
|
@ -5,7 +5,7 @@
|
||||
{{range $i, $v := .TreeNames}}
|
||||
<div class="breadcrumb-divider">/</div>
|
||||
{{if eq $i $l}}
|
||||
<input id="file-name" maxlength="255" value="{{$v}}" placeholder="{{ctx.Locale.Tr (Iif $.PageIsUpload "repo.editor.add_subdir" "repo.editor.name_your_file")}}" data-editorconfig="{{$.EditorconfigJson}}" required autofocus>
|
||||
<input id="file-name" maxlength="255" value="{{$v}}" placeholder="{{ctx.Locale.Tr (Iif $.PageIsUpload "repo.editor.add_subdir" "repo.editor.name_your_file")}}" data-editorconfig="{{$.EditorconfigJson}}" {{Iif $.PageIsUpload "" "required"}} autofocus>
|
||||
<span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span>
|
||||
{{else}}
|
||||
<span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span>
|
||||
|
@ -22,7 +22,7 @@
|
||||
<span class="label-filter-exclude-info">{{ctx.Locale.Tr "repo.issues.filter_label_exclude"}}</span>
|
||||
<div class="divider"></div>
|
||||
<a class="item label-filter-query-default" href="{{QueryBuild $queryLink "labels" NIL}}">{{ctx.Locale.Tr "repo.issues.filter_label_no_select"}}</a>
|
||||
<a class="item label-filter-query-not-set" href="{{QueryBuild $queryLink "labels" 0}}">{{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}}</a>
|
||||
<a class="item label-filter-query-not-set" href="{{QueryBuild $queryLink "labels" "0"}}">{{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}}</a>
|
||||
{{/* The logic here is not the same as the label selector in the issue sidebar.
|
||||
The one in the issue sidebar renders "repo labels | divider | org labels".
|
||||
Maybe the logic should be updated to be consistent.*/}}
|
||||
|
@ -15,7 +15,7 @@
|
||||
<input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_milestone"}}">
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<a class="{{if not $.MilestoneID}}active selected {{end}}item" href="{{QueryBuild $queryLink "milestone" 0}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_all"}}</a>
|
||||
<a class="{{if not $.MilestoneID}}active selected {{end}}item" href="{{QueryBuild $queryLink "milestone" NIL}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_all"}}</a>
|
||||
<a class="{{if $.MilestoneID}}{{if eq $.MilestoneID -1}}active selected {{end}}{{end}}item" href="{{QueryBuild $queryLink "milestone" -1}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_none"}}</a>
|
||||
{{if .OpenMilestones}}
|
||||
<div class="divider"></div>
|
||||
|
@ -30,8 +30,6 @@
|
||||
<audio controls src="{{$.RawFileLink}}">
|
||||
<strong>{{ctx.Locale.Tr "repo.audio_not_supported_in_browser"}}</strong>
|
||||
</audio>
|
||||
{{else if .IsPDFFile}}
|
||||
<div class="pdf-content is-loading" data-global-init="initPdfViewer" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "diff.view_file"}}"></div>
|
||||
{{else}}
|
||||
<a href="{{$.RawFileLink}}" rel="nofollow" class="tw-p-4">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
||||
{{end}}
|
||||
|
@ -5,9 +5,7 @@
|
||||
{{template "base/alert" .}}
|
||||
{{template "repo/release_tag_header" .}}
|
||||
<h4 class="ui top attached header">
|
||||
<div class="five wide column tw-flex tw-items-center">
|
||||
{{.TagCount}} {{ctx.Locale.Tr "repo.release.tags"}}
|
||||
</div>
|
||||
{{.TagCount}} {{ctx.Locale.Tr "repo.release.tags"}}
|
||||
</h4>
|
||||
{{$canReadReleases := $.Permission.CanRead ctx.Consts.RepoUnitTypeReleases}}
|
||||
<div class="ui attached segment">
|
||||
@ -15,53 +13,49 @@
|
||||
{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.tag_kind") "Tooltip" (ctx.Locale.Tr "search.tag_tooltip")}}
|
||||
</form>
|
||||
</div>
|
||||
<div class="ui attached table segment">
|
||||
<div class="ui attached segment tw-p-0">
|
||||
{{if .Releases}}
|
||||
<table class="ui very basic striped fixed table single line" id="tags-table">
|
||||
<tbody class="tag-list">
|
||||
{{range $idx, $release := .Releases}}
|
||||
<tr>
|
||||
<td class="tag-list-row">
|
||||
<h3 class="tag-list-row-title tw-mb-2">
|
||||
{{if $canReadReleases}}
|
||||
<a class="tag-list-row-link tw-flex tw-items-center" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a>
|
||||
{{else}}
|
||||
<a class="tag-list-row-link tw-flex tw-items-center" href="{{$.RepoLink}}/src/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a>
|
||||
{{end}}
|
||||
</h3>
|
||||
<div class="download tw-flex tw-items-center">
|
||||
{{if $.Permission.CanRead ctx.Consts.RepoUnitTypeCode}}
|
||||
{{if .CreatedUnix}}
|
||||
<span class="tw-mr-2">{{svg "octicon-clock" 16 "tw-mr-1"}}{{DateUtils.TimeSince .CreatedUnix}}</span>
|
||||
{{end}}
|
||||
<div class="ui divided list" id="tags-table">
|
||||
{{range $idx, $release := .Releases}}
|
||||
<div class="item tag-list-row tw-p-4">
|
||||
<h3 class="tag-list-row-title tw-mb-2">
|
||||
{{if $canReadReleases}}
|
||||
<a class="tag-list-row-link" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a>
|
||||
{{else}}
|
||||
<a class="tag-list-row-link" href="{{$.RepoLink}}/src/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a>
|
||||
{{end}}
|
||||
</h3>
|
||||
<div class="flex-text-block muted-links tw-gap-4 tw-flex-wrap">
|
||||
{{if $.Permission.CanRead ctx.Consts.RepoUnitTypeCode}}
|
||||
{{if .CreatedUnix}}
|
||||
<span class="flex-text-inline">{{svg "octicon-clock"}}{{DateUtils.TimeSince .CreatedUnix}}</span>
|
||||
{{end}}
|
||||
|
||||
<a class="tw-mr-2 tw-font-mono muted" href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha .Sha1}}</a>
|
||||
<a class="flex-text-inline tw-font-mono" href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow">{{svg "octicon-git-commit"}}{{ShortSha .Sha1}}</a>
|
||||
|
||||
{{if not $.DisableDownloadSourceArchives}}
|
||||
<a class="archive-link tw-mr-2 muted" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-1"}}ZIP</a>
|
||||
<a class="archive-link tw-mr-2 muted" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-1"}}TAR.GZ</a>
|
||||
{{end}}
|
||||
{{if not $.DisableDownloadSourceArchives}}
|
||||
<a class="archive-link flex-text-inline" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.zip" rel="nofollow">{{svg "octicon-file-zip"}}ZIP</a>
|
||||
<a class="archive-link flex-text-inline" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip"}}TAR.GZ</a>
|
||||
{{end}}
|
||||
|
||||
{{if (and $canReadReleases $.CanCreateRelease $release.IsTag)}}
|
||||
<a class="tw-mr-2 muted" href="{{$.RepoLink}}/releases/new?tag={{.TagName}}">{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.new_release"}}</a>
|
||||
{{end}}
|
||||
{{if (and $canReadReleases $.CanCreateRelease $release.IsTag)}}
|
||||
<a class="flex-text-inline" href="{{$.RepoLink}}/releases/new?tag={{.TagName}}">{{svg "octicon-tag"}}{{ctx.Locale.Tr "repo.release.new_release"}}</a>
|
||||
{{end}}
|
||||
|
||||
{{if (and ($.Permission.CanWrite ctx.Consts.RepoUnitTypeCode) $release.IsTag)}}
|
||||
<a class="ui delete-button tw-mr-2 muted" data-url="{{$.RepoLink}}/tags/delete" data-id="{{.ID}}">
|
||||
{{svg "octicon-trash" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.delete_tag"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if (and ($.Permission.CanWrite ctx.Consts.RepoUnitTypeCode) $release.IsTag)}}
|
||||
<a class="flex-text-inline link-action" data-url="{{$.RepoLink}}/tags/delete?id={{.ID}}" data-modal-confirm="#confirm-delete-tag-modal">
|
||||
{{svg "octicon-trash"}}{{ctx.Locale.Tr "repo.release.delete_tag"}}
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if and $canReadReleases (not $release.IsTag)}}
|
||||
<a class="tw-mr-2 muted" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}">{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.detail"}}</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{if and $canReadReleases (not $release.IsTag)}}
|
||||
<a class="flex-text-inline" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}">{{svg "octicon-tag"}}{{ctx.Locale.Tr "repo.release.detail"}}</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
{{if .NumTags}}
|
||||
<p class="tw-p-4">{{ctx.Locale.Tr "no_results_found"}}</p>
|
||||
@ -73,9 +67,8 @@
|
||||
</div>
|
||||
|
||||
{{if $.Permission.CanWrite ctx.Consts.RepoUnitTypeCode}}
|
||||
<div class="ui g-modal-confirm delete modal">
|
||||
<div id="confirm-delete-tag-modal" class="ui small modal">
|
||||
<div class="header">
|
||||
{{svg "octicon-trash"}}
|
||||
{{ctx.Locale.Tr "repo.release.delete_tag"}}
|
||||
</div>
|
||||
<div class="content">
|
||||
|
@ -1,4 +1,6 @@
|
||||
<div {{if .ReadmeInList}}id="readme" {{end}}class="{{TabSizeClass .Editorconfig .FileTreePath}} non-diff-file-content">
|
||||
<div {{if .ReadmeInList}}id="readme"{{end}} class="{{TabSizeClass .Editorconfig .FileTreePath}} non-diff-file-content"
|
||||
data-global-init="initRepoFileView" data-raw-file-link="{{.RawFileLink}}">
|
||||
|
||||
{{- if .FileError}}
|
||||
<div class="ui error message">
|
||||
<div class="text left tw-whitespace-pre">{{.FileError}}</div>
|
||||
@ -32,13 +34,14 @@
|
||||
{{template "repo/file_info" .}}
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="file-header-right file-actions tw-flex tw-items-center tw-flex-wrap">
|
||||
{{if .HasSourceRenderedToggle}}
|
||||
<div class="ui compact icon buttons">
|
||||
<a href="?display=source" class="ui mini basic button {{if .IsDisplayingSource}}active{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_source"}}">{{svg "octicon-code" 15}}</a>
|
||||
<a href="{{$.Link}}" class="ui mini basic button {{if .IsDisplayingRendered}}active{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_rendered"}}">{{svg "octicon-file" 15}}</a>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="file-header-right file-actions flex-text-block tw-flex-wrap">
|
||||
{{/* this componment is also controlled by frontend plugin renders */}}
|
||||
<div class="ui compact icon buttons file-view-toggle-buttons {{Iif .HasSourceRenderedToggle "" "tw-hidden"}}">
|
||||
{{if .IsRepresentableAsText}}
|
||||
<a href="?display=source" class="ui mini basic button file-view-toggle-source {{if .IsDisplayingSource}}active{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_source"}}">{{svg "octicon-code" 15}}</a>
|
||||
{{end}}
|
||||
<a href="?display=rendered" class="ui mini basic button file-view-toggle-rendered {{if not .IsDisplayingSource}}active{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_rendered"}}">{{svg "octicon-file" 15}}</a>
|
||||
</div>
|
||||
{{if not .ReadmeInList}}
|
||||
<div class="ui buttons tw-mr-1">
|
||||
<a class="ui mini basic button" href="{{$.RawFileLink}}">{{ctx.Locale.Tr "repo.file_raw"}}</a>
|
||||
@ -55,7 +58,10 @@
|
||||
{{end}}
|
||||
</div>
|
||||
<a download class="btn-octicon" data-tooltip-content="{{ctx.Locale.Tr "repo.download_file"}}" href="{{$.RawFileLink}}">{{svg "octicon-download"}}</a>
|
||||
<a class="btn-octicon {{if not .CanCopyContent}} disabled{{end}}" data-global-click="onCopyContentButtonClick" {{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>
|
||||
<a class="btn-octicon {{if not .CanCopyContent}}disabled{{end}}" data-global-click="onCopyContentButtonClick"
|
||||
{{if not .IsDisplayingSource}}data-raw-file-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}}
|
||||
<a class="btn-octicon" href="{{$.RepoLink}}/rss/{{$.RefTypeNameSubURL}}/{{PathEscapeSegments .TreePath}}" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">
|
||||
{{svg "octicon-rss"}}
|
||||
@ -82,20 +88,36 @@
|
||||
{{end}}
|
||||
</div>
|
||||
</h4>
|
||||
|
||||
<div class="ui bottom attached table unstackable segment">
|
||||
{{if not (or .IsMarkup .IsRenderedHTML)}}
|
||||
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}}
|
||||
{{if not .IsMarkup}}
|
||||
{{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}}
|
||||
{{end}}
|
||||
<div class="file-view{{if .IsMarkup}} markup {{.MarkupType}}{{else if .IsPlainText}} plain-text{{else if .IsTextSource}} code-view{{end}}">
|
||||
<div class="file-view {{if .IsMarkup}}markup {{.MarkupType}}{{else if .IsPlainText}}plain-text{{else if .IsDisplayingSource}}code-view{{end}}">
|
||||
{{if .IsFileTooLarge}}
|
||||
{{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}}
|
||||
{{else if not .FileSize}}
|
||||
{{template "shared/fileisempty"}}
|
||||
{{else if .IsMarkup}}
|
||||
{{if .FileContent}}{{.FileContent}}{{end}}
|
||||
{{.FileContent}}
|
||||
{{else if .IsPlainText}}
|
||||
<pre>{{if .FileContent}}{{.FileContent}}{{end}}</pre>
|
||||
{{else if not .IsTextSource}}
|
||||
{{else if .FileContent}}
|
||||
<table>
|
||||
<tbody>
|
||||
{{range $idx, $code := .FileContent}}
|
||||
{{$line := Eval $idx "+" 1}}
|
||||
<tr>
|
||||
<td id="L{{$line}}" class="lines-num"><span id="L{{$line}}" data-line-number="{{$line}}"></span></td>
|
||||
{{if $.EscapeStatus.Escaped}}
|
||||
<td class="lines-escape">{{if (index $.LineEscapeStatus $idx).Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{if (index $.LineEscapeStatus $idx).HasInvisible}}{{ctx.Locale.Tr "repo.invisible_runes_line"}} {{end}}{{if (index $.LineEscapeStatus $idx).HasAmbiguous}}{{ctx.Locale.Tr "repo.ambiguous_runes_line"}}{{end}}"></button>{{end}}</td>
|
||||
{{end}}
|
||||
<td rel="L{{$line}}" class="lines-code chroma"><code class="code-inner">{{$code}}</code></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="view-raw">
|
||||
{{if .IsImageFile}}
|
||||
<img alt="{{$.RawFileLink}}" src="{{$.RawFileLink}}">
|
||||
@ -107,35 +129,23 @@
|
||||
<audio controls src="{{$.RawFileLink}}">
|
||||
<strong>{{ctx.Locale.Tr "repo.audio_not_supported_in_browser"}}</strong>
|
||||
</audio>
|
||||
{{else if .IsPDFFile}}
|
||||
<div class="pdf-content is-loading" data-global-init="initPdfViewer" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "repo.diff.view_file"}}"></div>
|
||||
{{else}}
|
||||
<a href="{{$.RawFileLink}}" rel="nofollow" class="tw-p-4">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
||||
<div class="file-view-render-container">
|
||||
<div class="file-view-raw-prompt tw-p-4">
|
||||
<a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else if .FileSize}}
|
||||
<table>
|
||||
<tbody>
|
||||
{{range $idx, $code := .FileContent}}
|
||||
{{$line := Eval $idx "+" 1}}
|
||||
<tr>
|
||||
<td id="L{{$line}}" class="lines-num"><span id="L{{$line}}" data-line-number="{{$line}}"></span></td>
|
||||
{{if $.EscapeStatus.Escaped}}
|
||||
<td class="lines-escape">{{if (index $.LineEscapeStatus $idx).Escaped}}<button class="toggle-escape-button btn interact-bg" title="{{if (index $.LineEscapeStatus $idx).HasInvisible}}{{ctx.Locale.Tr "repo.invisible_runes_line"}} {{end}}{{if (index $.LineEscapeStatus $idx).HasAmbiguous}}{{ctx.Locale.Tr "repo.ambiguous_runes_line"}}{{end}}"></button>{{end}}</td>
|
||||
{{end}}
|
||||
<td rel="L{{$line}}" class="lines-code chroma"><code class="code-inner">{{$code}}</code></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="code-line-menu tippy-target">
|
||||
{{if $.Permission.CanRead ctx.Consts.RepoUnitTypeIssues}}
|
||||
<a class="item ref-in-new-issue" role="menuitem" data-url-issue-new="{{.RepoLink}}/issues/new" data-url-param-body-link="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}" rel="nofollow noindex">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</a>
|
||||
{{end}}
|
||||
<a class="item view_git_blame" role="menuitem" href="{{.Repository.Link}}/blame/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.view_git_blame"}}</a>
|
||||
<a class="item copy-line-permalink" role="menuitem" data-url="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}">{{ctx.Locale.Tr "repo.file_copy_permalink"}}</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="code-line-menu tippy-target">
|
||||
{{if $.Permission.CanRead ctx.Consts.RepoUnitTypeIssues}}
|
||||
<a class="item ref-in-new-issue" role="menuitem" data-url-issue-new="{{.RepoLink}}/issues/new" data-url-param-body-link="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}" rel="nofollow noindex">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</a>
|
||||
{{end}}
|
||||
<a class="item view_git_blame" role="menuitem" href="{{.Repository.Link}}/blame/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.view_git_blame"}}</a>
|
||||
<a class="item copy-line-permalink" role="menuitem" data-url="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}">{{ctx.Locale.Tr "repo.file_copy_permalink"}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
119
templates/swagger/v1_json.tmpl
generated
119
templates/swagger/v1_json.tmpl
generated
@ -766,7 +766,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "user's login name to search for",
|
||||
"description": "identifier of the user, provided by the external authenticator",
|
||||
"name": "login_name",
|
||||
"in": "query"
|
||||
},
|
||||
@ -842,7 +842,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user to delete",
|
||||
"description": "username of the user to delete",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -884,7 +884,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user to edit",
|
||||
"description": "username of the user whose data is to be edited",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -926,7 +926,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user",
|
||||
"description": "username of the user whose badges are to be listed",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -956,7 +956,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user",
|
||||
"description": "username of the user to whom a badge is to be added",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -990,7 +990,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user",
|
||||
"description": "username of the user whose badge is to be deleted",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -1032,7 +1032,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of the user",
|
||||
"description": "username of the user who is to receive a public key",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -1071,7 +1071,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user",
|
||||
"description": "username of the user whose public key is to be deleted",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -1114,7 +1114,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of the user that will own the created organization",
|
||||
"description": "username of the user who will own the created organization",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -1154,7 +1154,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "existing username of user",
|
||||
"description": "current username of the user",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -1197,7 +1197,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of the user. This user will own the created repository",
|
||||
"description": "username of the user who will own the created repository",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -2716,7 +2716,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "user to check",
|
||||
"description": "username of the user to check",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -2747,7 +2747,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "user to block",
|
||||
"description": "username of the user to block",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -2787,7 +2787,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "user to unblock",
|
||||
"description": "username of the user to unblock",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -3258,7 +3258,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of the user",
|
||||
"description": "username of the user to check for an organization membership",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -3295,7 +3295,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of the user",
|
||||
"description": "username of the user to remove from the organization",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -3369,7 +3369,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of the user",
|
||||
"description": "username of the user to check for a public organization membership",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -3403,7 +3403,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of the user",
|
||||
"description": "username of the user whose membership is to be publicized",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -3440,7 +3440,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of the user",
|
||||
"description": "username of the user whose membership is to be concealed",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -7046,7 +7046,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of the collaborator",
|
||||
"description": "username of the user to check for being a collaborator",
|
||||
"name": "collaborator",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -7090,7 +7090,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of the collaborator to add",
|
||||
"description": "username of the user to add or update as a collaborator",
|
||||
"name": "collaborator",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -7190,7 +7190,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of the collaborator",
|
||||
"description": "username of the collaborator whose permissions are to be obtained",
|
||||
"name": "collaborator",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -12067,7 +12067,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "user to subscribe",
|
||||
"description": "username of the user to subscribe the issue to",
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -12125,7 +12125,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "user witch unsubscribe",
|
||||
"description": "username of the user to unsubscribe from an issue",
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -16937,7 +16937,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user",
|
||||
"description": "username of the user whose tracked times are to be listed",
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -17960,7 +17960,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of the member to list",
|
||||
"description": "username of the user whose data is to be listed",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -17995,7 +17995,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of the user to add",
|
||||
"description": "username of the user to add to a team",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -18033,7 +18033,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of the user to remove",
|
||||
"description": "username of the user to remove from a team",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -19012,7 +19012,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "user to check",
|
||||
"description": "username of the user to check",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -19036,7 +19036,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "user to block",
|
||||
"description": "username of the user to block",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -19069,7 +19069,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "user to unblock",
|
||||
"description": "username of the user to unblock",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -19231,7 +19231,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of followed user",
|
||||
"description": "username of the user to check for authenticated followers",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -19255,7 +19255,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user to follow",
|
||||
"description": "username of the user to follow",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -19282,7 +19282,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user to unfollow",
|
||||
"description": "username of the user to unfollow",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -20236,7 +20236,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user to get",
|
||||
"description": "username of the user whose data is to be listed",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -20265,7 +20265,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user",
|
||||
"description": "username of the user whose activity feeds are to be listed",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -20319,7 +20319,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user",
|
||||
"description": "username of the user whose followers are to be listed",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -20360,7 +20360,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user",
|
||||
"description": "username of the user whose followed users are to be listed",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -20398,14 +20398,14 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of following user",
|
||||
"description": "username of the following user",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of followed user",
|
||||
"description": "username of the followed user",
|
||||
"name": "target",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -20434,7 +20434,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user",
|
||||
"description": "username of the user whose GPG key list is to be obtained",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -20475,7 +20475,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user to get",
|
||||
"description": "username of the user whose heatmap is to be obtained",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -20504,7 +20504,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user",
|
||||
"description": "username of the user whose public keys are to be listed",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -20551,7 +20551,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user",
|
||||
"description": "username of the user whose organizations are to be listed",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -20592,7 +20592,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user",
|
||||
"description": "username of the user whose permissions are to be obtained",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -20631,7 +20631,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user",
|
||||
"description": "username of the user whose owned repos are to be listed",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -20672,7 +20672,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user",
|
||||
"description": "username of the user whose starred repos are to be listed",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -20716,7 +20716,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of the user",
|
||||
"description": "username of the user whose watched repos are to be listed",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -20757,7 +20757,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user",
|
||||
"description": "username of to user whose access tokens are to be listed",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -20799,7 +20799,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user",
|
||||
"description": "username of the user whose token is to be created",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -20838,7 +20838,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of user",
|
||||
"description": "username of the user whose token is to be deleted",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
@ -21639,7 +21639,7 @@
|
||||
"x-go-name": "Time"
|
||||
},
|
||||
"user_name": {
|
||||
"description": "User who spent the time (optional)",
|
||||
"description": "username of the user who spent the time working on the issue (optional)",
|
||||
"type": "string",
|
||||
"x-go-name": "User"
|
||||
}
|
||||
@ -23174,6 +23174,7 @@
|
||||
"x-go-name": "RepoAdminChangeTeamAccess"
|
||||
},
|
||||
"username": {
|
||||
"description": "username of the organization",
|
||||
"type": "string",
|
||||
"x-go-name": "UserName"
|
||||
},
|
||||
@ -23624,7 +23625,9 @@
|
||||
"x-go-name": "FullName"
|
||||
},
|
||||
"login_name": {
|
||||
"description": "identifier of the user, provided by the external authenticator (if configured)",
|
||||
"type": "string",
|
||||
"default": "empty",
|
||||
"x-go-name": "LoginName"
|
||||
},
|
||||
"must_change_password": {
|
||||
@ -23649,6 +23652,7 @@
|
||||
"x-go-name": "SourceID"
|
||||
},
|
||||
"username": {
|
||||
"description": "username of the user",
|
||||
"type": "string",
|
||||
"x-go-name": "Username"
|
||||
},
|
||||
@ -24634,7 +24638,9 @@
|
||||
"x-go-name": "Location"
|
||||
},
|
||||
"login_name": {
|
||||
"description": "identifier of the user, provided by the external authenticator (if configured)",
|
||||
"type": "string",
|
||||
"default": "empty",
|
||||
"x-go-name": "LoginName"
|
||||
},
|
||||
"max_repo_creation": {
|
||||
@ -24693,6 +24699,7 @@
|
||||
"x-go-name": "UserID"
|
||||
},
|
||||
"username": {
|
||||
"description": "username of the user",
|
||||
"type": "string",
|
||||
"x-go-name": "UserName"
|
||||
},
|
||||
@ -26417,7 +26424,7 @@
|
||||
"x-go-name": "RepoAdminChangeTeamAccess"
|
||||
},
|
||||
"username": {
|
||||
"description": "deprecated",
|
||||
"description": "username of the organization\ndeprecated",
|
||||
"type": "string",
|
||||
"x-go-name": "UserName"
|
||||
},
|
||||
@ -26661,6 +26668,7 @@
|
||||
"x-go-name": "Name"
|
||||
},
|
||||
"username": {
|
||||
"description": "username of the user",
|
||||
"type": "string",
|
||||
"x-go-name": "UserName"
|
||||
}
|
||||
@ -28124,6 +28132,7 @@
|
||||
"x-go-name": "UserID"
|
||||
},
|
||||
"user_name": {
|
||||
"description": "username of the user",
|
||||
"type": "string",
|
||||
"x-go-name": "UserName"
|
||||
}
|
||||
@ -28365,12 +28374,12 @@
|
||||
"x-go-name": "Location"
|
||||
},
|
||||
"login": {
|
||||
"description": "the user's username",
|
||||
"description": "login of the user, same as `username`",
|
||||
"type": "string",
|
||||
"x-go-name": "UserName"
|
||||
},
|
||||
"login_name": {
|
||||
"description": "the user's authentication sign-in name.",
|
||||
"description": "identifier of the user, provided by the external authenticator (if configured)",
|
||||
"type": "string",
|
||||
"default": "empty",
|
||||
"x-go-name": "LoginName"
|
||||
|
@ -157,9 +157,14 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
|
||||
t.Run("Download", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// download the package without the file name
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s", groupURL, packageName, packageVersion, packageArchitecture))
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, content, resp.Body.Bytes())
|
||||
|
||||
// download the package with a file name (it can be anything)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s/any-file-name", groupURL, packageName, packageVersion, packageArchitecture))
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, content, resp.Body.Bytes())
|
||||
})
|
||||
|
||||
@ -447,7 +452,8 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
|
||||
pub, err := openpgp.ReadArmoredKeyRing(gpgResp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s", groupURL, packageName, packageVersion, packageArchitecture))
|
||||
rpmFileName := fmt.Sprintf("%s-%s.%s.rpm", packageName, packageVersion, packageArchitecture)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s/%s", groupURL, packageName, packageVersion, packageArchitecture, rpmFileName))
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
_, sigs, err := rpmutils.Verify(resp.Body, pub)
|
||||
|
@ -68,14 +68,15 @@ func TestLFSRender(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/user2/lfs/src/branch/master/crypt.bin")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
doc := NewHTMLParser(t, resp.Body).doc
|
||||
doc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
fileInfo := doc.Find("div.file-info-entry").First().Text()
|
||||
assert.Contains(t, fileInfo, "LFS")
|
||||
|
||||
rawLink, exists := doc.Find("div.file-view > div.view-raw > a").Attr("href")
|
||||
assert.True(t, exists, "Download link should render instead of content because this is a binary file")
|
||||
assert.Equal(t, "/user2/lfs/media/branch/master/crypt.bin", rawLink, "The download link should use the proper /media link because it's in LFS")
|
||||
// find new file view container
|
||||
fileViewContainer := doc.Find("[data-global-init=initRepoFileView]")
|
||||
assert.Equal(t, "/user2/lfs/media/branch/master/crypt.bin", fileViewContainer.AttrOr("data-raw-file-link", ""))
|
||||
AssertHTMLElement(t, doc, ".view-raw > .file-view-render-container > .file-view-raw-prompt", 1)
|
||||
})
|
||||
|
||||
// check that a directory with a README file shows its text
|
||||
|
@ -52,8 +52,7 @@ form.single-button-form.is-loading .button {
|
||||
}
|
||||
|
||||
.markup pre.is-loading,
|
||||
.editor-loading.is-loading,
|
||||
.pdf-content.is-loading {
|
||||
.editor-loading.is-loading {
|
||||
height: var(--height-loading);
|
||||
}
|
||||
|
||||
|
@ -183,42 +183,6 @@ td .commit-summary {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.view-raw {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.view-raw > * {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.view-raw audio,
|
||||
.view-raw video,
|
||||
.view-raw img {
|
||||
margin: 1rem 0;
|
||||
border-radius: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.view-raw img[src$=".svg" i] {
|
||||
max-height: 600px !important;
|
||||
max-width: 600px !important;
|
||||
}
|
||||
|
||||
.pdf-content {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
border: none !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pdf-content .pdf-fallback-button {
|
||||
margin: 50px auto;
|
||||
}
|
||||
|
||||
.repository.file.list .non-diff-file-content .plain-text {
|
||||
padding: 1em 2em;
|
||||
}
|
||||
@ -241,10 +205,6 @@ td .commit-summary {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.non-diff-file-content .pdfobject {
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
}
|
||||
|
||||
.repo-editor-header {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -60,3 +60,33 @@
|
||||
.file-view.code-view .ui.button.code-line-button:hover {
|
||||
background: var(--color-secondary);
|
||||
}
|
||||
|
||||
.view-raw {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.view-raw > * {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.view-raw audio,
|
||||
.view-raw video,
|
||||
.view-raw img {
|
||||
margin: 1rem;
|
||||
border-radius: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.view-raw img[src$=".svg" i] {
|
||||
max-height: 600px !important;
|
||||
max-width: 600px !important;
|
||||
}
|
||||
|
||||
.file-view-render-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-view-render-container :last-child {
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius); /* to match the "ui segment" bottom radius */
|
||||
}
|
||||
|
@ -91,10 +91,6 @@
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#tags-table .tag-list-row {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
#tags-table .tag-list-row-title {
|
||||
font-size: 18px;
|
||||
font-weight: var(--font-weight-normal);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {request} from '../modules/fetch.ts';
|
||||
import {hideToastsAll, showErrorToast} from '../modules/toast.ts';
|
||||
import {addDelegatedEventListener, submitEventSubmitter} from '../utils/dom.ts';
|
||||
import {confirmModal} from './comp/ConfirmModal.ts';
|
||||
import {addDelegatedEventListener, createElementFromHTML, submitEventSubmitter} from '../utils/dom.ts';
|
||||
import {confirmModal, createConfirmModal} from './comp/ConfirmModal.ts';
|
||||
import type {RequestOpts} from '../types.ts';
|
||||
import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
|
||||
|
||||
@ -111,28 +111,44 @@ export async function submitFormFetchAction(formEl: HTMLFormElement, formSubmitt
|
||||
async function onLinkActionClick(el: HTMLElement, e: Event) {
|
||||
// A "link-action" can post AJAX request to its "data-url"
|
||||
// Then the browser is redirected to: the "redirect" in response, or "data-redirect" attribute, or current URL by reloading.
|
||||
// If the "link-action" has "data-modal-confirm" attribute, a confirm modal dialog will be shown before taking action.
|
||||
// If the "link-action" has "data-modal-confirm" attribute, a "confirm modal dialog" will be shown before taking action.
|
||||
// Attribute "data-modal-confirm" can be a modal element by "#the-modal-id", or a string content for the modal dialog.
|
||||
e.preventDefault();
|
||||
const url = el.getAttribute('data-url');
|
||||
const doRequest = async () => {
|
||||
if ('disabled' in el) el.disabled = true; // el could be A or BUTTON, but A doesn't have disabled attribute
|
||||
if ('disabled' in el) el.disabled = true; // el could be A or BUTTON, but "A" doesn't have the "disabled" attribute
|
||||
await fetchActionDoRequest(el, url, {method: el.getAttribute('data-link-action-method') || 'POST'});
|
||||
if ('disabled' in el) el.disabled = false;
|
||||
};
|
||||
|
||||
const modalConfirmContent = el.getAttribute('data-modal-confirm') ||
|
||||
el.getAttribute('data-modal-confirm-content') || '';
|
||||
if (!modalConfirmContent) {
|
||||
let elModal: HTMLElement | null = null;
|
||||
const dataModalConfirm = el.getAttribute('data-modal-confirm') || '';
|
||||
if (dataModalConfirm.startsWith('#')) {
|
||||
// eslint-disable-next-line unicorn/prefer-query-selector
|
||||
elModal = document.getElementById(dataModalConfirm.substring(1));
|
||||
if (elModal) {
|
||||
elModal = createElementFromHTML(elModal.outerHTML);
|
||||
elModal.removeAttribute('id');
|
||||
}
|
||||
}
|
||||
if (!elModal) {
|
||||
const modalConfirmContent = dataModalConfirm || el.getAttribute('data-modal-confirm-content') || '';
|
||||
if (modalConfirmContent) {
|
||||
const isRisky = el.classList.contains('red') || el.classList.contains('negative');
|
||||
elModal = createConfirmModal({
|
||||
header: el.getAttribute('data-modal-confirm-header') || '',
|
||||
content: modalConfirmContent,
|
||||
confirmButtonColor: isRisky ? 'red' : 'primary',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!elModal) {
|
||||
await doRequest();
|
||||
return;
|
||||
}
|
||||
|
||||
const isRisky = el.classList.contains('red') || el.classList.contains('negative');
|
||||
if (await confirmModal({
|
||||
header: el.getAttribute('data-modal-confirm-header') || '',
|
||||
content: modalConfirmContent,
|
||||
confirmButtonColor: isRisky ? 'red' : 'primary',
|
||||
})) {
|
||||
if (await confirmModal(elModal)) {
|
||||
await doRequest();
|
||||
}
|
||||
}
|
||||
|
@ -5,20 +5,29 @@ import {fomanticQuery} from '../../modules/fomantic/base.ts';
|
||||
|
||||
const {i18n} = window.config;
|
||||
|
||||
export function confirmModal({header = '', content = '', confirmButtonColor = 'primary'} = {}): Promise<boolean> {
|
||||
type ConfirmModalOptions = {
|
||||
header?: string;
|
||||
content?: string;
|
||||
confirmButtonColor?: 'primary' | 'red' | 'green' | 'blue';
|
||||
}
|
||||
|
||||
export function createConfirmModal({header = '', content = '', confirmButtonColor = 'primary'}:ConfirmModalOptions = {}): HTMLElement {
|
||||
const headerHtml = header ? `<div class="header">${htmlEscape(header)}</div>` : '';
|
||||
return createElementFromHTML(`
|
||||
<div class="ui g-modal-confirm modal">
|
||||
${headerHtml}
|
||||
<div class="content">${htmlEscape(content)}</div>
|
||||
<div class="actions">
|
||||
<button class="ui cancel button">${svg('octicon-x')} ${htmlEscape(i18n.modal_cancel)}</button>
|
||||
<button class="ui ${confirmButtonColor} ok button">${svg('octicon-check')} ${htmlEscape(i18n.modal_confirm)}</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
export function confirmModal(modal: HTMLElement | ConfirmModalOptions): Promise<boolean> {
|
||||
if (!(modal instanceof HTMLElement)) modal = createConfirmModal(modal);
|
||||
return new Promise((resolve) => {
|
||||
const headerHtml = header ? `<div class="header">${htmlEscape(header)}</div>` : '';
|
||||
const modal = createElementFromHTML(`
|
||||
<div class="ui g-modal-confirm modal">
|
||||
${headerHtml}
|
||||
<div class="content">${htmlEscape(content)}</div>
|
||||
<div class="actions">
|
||||
<button class="ui cancel button">${svg('octicon-x')} ${htmlEscape(i18n.modal_cancel)}</button>
|
||||
<button class="ui ${confirmButtonColor} ok button">${svg('octicon-check')} ${htmlEscape(i18n.modal_confirm)}</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
document.body.append(modal);
|
||||
const $modal = fomanticQuery(modal);
|
||||
$modal.modal({
|
||||
onApprove() {
|
||||
|
@ -9,17 +9,17 @@ const {i18n} = window.config;
|
||||
export function initCopyContent() {
|
||||
registerGlobalEventFunc('click', 'onCopyContentButtonClick', async (btn: HTMLElement) => {
|
||||
if (btn.classList.contains('disabled') || btn.classList.contains('is-loading')) return;
|
||||
let content;
|
||||
let isRasterImage = false;
|
||||
const link = btn.getAttribute('data-link');
|
||||
const rawFileLink = btn.getAttribute('data-raw-file-link');
|
||||
|
||||
// when data-link is present, we perform a fetch. this is either because
|
||||
// the text to copy is not in the DOM, or it is an image which should be
|
||||
let content, isRasterImage = false;
|
||||
|
||||
// when "data-raw-link" is present, we perform a fetch. this is either because
|
||||
// the text to copy is not in the DOM, or it is an image that should be
|
||||
// fetched to copy in full resolution
|
||||
if (link) {
|
||||
if (rawFileLink) {
|
||||
btn.classList.add('is-loading', 'loading-icon-2px');
|
||||
try {
|
||||
const res = await GET(link, {credentials: 'include', redirect: 'follow'});
|
||||
const res = await GET(rawFileLink, {credentials: 'include', redirect: 'follow'});
|
||||
const contentType = res.headers.get('content-type');
|
||||
|
||||
if (contentType.startsWith('image/') && !contentType.startsWith('image/svg')) {
|
||||
|
76
web_src/js/features/file-view.ts
Normal file
76
web_src/js/features/file-view.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import type {FileRenderPlugin} from '../render/plugin.ts';
|
||||
import {newRenderPlugin3DViewer} from '../render/plugins/3d-viewer.ts';
|
||||
import {newRenderPluginPdfViewer} from '../render/plugins/pdf-viewer.ts';
|
||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
import {createElementFromHTML, showElem, toggleClass} from '../utils/dom.ts';
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
import {basename} from '../utils.ts';
|
||||
|
||||
const plugins: FileRenderPlugin[] = [];
|
||||
|
||||
function initPluginsOnce(): void {
|
||||
if (plugins.length) return;
|
||||
plugins.push(newRenderPlugin3DViewer(), newRenderPluginPdfViewer());
|
||||
}
|
||||
|
||||
function findFileRenderPlugin(filename: string, mimeType: string): FileRenderPlugin | null {
|
||||
return plugins.find((plugin) => plugin.canHandle(filename, mimeType)) || null;
|
||||
}
|
||||
|
||||
function showRenderRawFileButton(elFileView: HTMLElement, renderContainer: HTMLElement | null): void {
|
||||
const toggleButtons = elFileView.querySelector('.file-view-toggle-buttons');
|
||||
showElem(toggleButtons);
|
||||
const displayingRendered = Boolean(renderContainer);
|
||||
toggleClass(toggleButtons.querySelectorAll('.file-view-toggle-source'), 'active', !displayingRendered); // it may not exist
|
||||
toggleClass(toggleButtons.querySelector('.file-view-toggle-rendered'), 'active', displayingRendered);
|
||||
// TODO: if there is only one button, hide it?
|
||||
}
|
||||
|
||||
async function renderRawFileToContainer(container: HTMLElement, rawFileLink: string, mimeType: string) {
|
||||
const elViewRawPrompt = container.querySelector('.file-view-raw-prompt');
|
||||
if (!rawFileLink || !elViewRawPrompt) throw new Error('unexpected file view container');
|
||||
|
||||
let rendered = false, errorMsg = '';
|
||||
try {
|
||||
const plugin = findFileRenderPlugin(basename(rawFileLink), mimeType);
|
||||
if (plugin) {
|
||||
container.classList.add('is-loading');
|
||||
container.setAttribute('data-render-name', plugin.name); // not used yet
|
||||
await plugin.render(container, rawFileLink);
|
||||
rendered = true;
|
||||
}
|
||||
} catch (e) {
|
||||
errorMsg = `${e}`;
|
||||
} finally {
|
||||
container.classList.remove('is-loading');
|
||||
}
|
||||
|
||||
if (rendered) {
|
||||
elViewRawPrompt.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// remove all children from the container, and only show the raw file link
|
||||
container.replaceChildren(elViewRawPrompt);
|
||||
|
||||
if (errorMsg) {
|
||||
const elErrorMessage = createElementFromHTML(htmlEscape`<div class="ui error message">${errorMsg}</div>`);
|
||||
elViewRawPrompt.insertAdjacentElement('afterbegin', elErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
export function initRepoFileView(): void {
|
||||
registerGlobalInitFunc('initRepoFileView', async (elFileView: HTMLElement) => {
|
||||
initPluginsOnce();
|
||||
const rawFileLink = elFileView.getAttribute('data-raw-file-link');
|
||||
const mimeType = elFileView.getAttribute('data-mime-type') || ''; // not used yet
|
||||
// TODO: we should also provide the prefetched file head bytes to let the plugin decide whether to render or not
|
||||
const plugin = findFileRenderPlugin(basename(rawFileLink), mimeType);
|
||||
if (!plugin) return;
|
||||
|
||||
const renderContainer = elFileView.querySelector<HTMLElement>('.file-view-render-container');
|
||||
showRenderRawFileButton(elFileView, renderContainer);
|
||||
// maybe in the future multiple plugins can render the same file, so we should not assume only one plugin will render it
|
||||
if (renderContainer) await renderRawFileToContainer(renderContainer, rawFileLink, mimeType);
|
||||
});
|
||||
}
|
@ -19,7 +19,7 @@ import {initRepoIssueContentHistory} from './features/repo-issue-content.ts';
|
||||
import {initStopwatch} from './features/stopwatch.ts';
|
||||
import {initFindFileInRepo} from './features/repo-findfile.ts';
|
||||
import {initMarkupContent} from './markup/content.ts';
|
||||
import {initPdfViewer} from './render/pdf.ts';
|
||||
import {initRepoFileView} from './features/file-view.ts';
|
||||
import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts';
|
||||
import {initRepoPullRequestAllowMaintainerEdit, initRepoPullRequestReview, initRepoIssueSidebarDependency, initRepoIssueFilterItemLabel} from './features/repo-issue.ts';
|
||||
import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts';
|
||||
@ -159,10 +159,11 @@ onDomReady(() => {
|
||||
initUserAuthWebAuthnRegister,
|
||||
initUserSettings,
|
||||
initRepoDiffView,
|
||||
initPdfViewer,
|
||||
initColorPickers,
|
||||
|
||||
initOAuth2SettingsDisableCheckbox,
|
||||
|
||||
initRepoFileView,
|
||||
]);
|
||||
|
||||
// it must be the last one, then the "querySelectorAll" only needs to be executed once for global init functions.
|
||||
|
@ -1,17 +0,0 @@
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
|
||||
export async function initPdfViewer() {
|
||||
registerGlobalInitFunc('initPdfViewer', async (el: HTMLInputElement) => {
|
||||
const pdfobject = await import(/* webpackChunkName: "pdfobject" */'pdfobject');
|
||||
|
||||
const src = el.getAttribute('data-src');
|
||||
const fallbackText = el.getAttribute('data-fallback-button-text');
|
||||
pdfobject.embed(src, el, {
|
||||
fallbackLink: htmlEscape`
|
||||
<a role="button" class="ui basic button pdf-fallback-button" href="[url]">${fallbackText}</a>
|
||||
`,
|
||||
});
|
||||
el.classList.remove('is-loading');
|
||||
});
|
||||
}
|
10
web_src/js/render/plugin.ts
Normal file
10
web_src/js/render/plugin.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export type FileRenderPlugin = {
|
||||
// unique plugin name
|
||||
name: string;
|
||||
|
||||
// test if plugin can handle a specified file
|
||||
canHandle: (filename: string, mimeType: string) => boolean;
|
||||
|
||||
// render file content
|
||||
render: (container: HTMLElement, fileUrl: string, options?: any) => Promise<void>;
|
||||
}
|
60
web_src/js/render/plugins/3d-viewer.ts
Normal file
60
web_src/js/render/plugins/3d-viewer.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import type {FileRenderPlugin} from '../plugin.ts';
|
||||
import {extname} from '../../utils.ts';
|
||||
|
||||
// support common 3D model file formats, use online-3d-viewer library for rendering
|
||||
|
||||
// eslint-disable-next-line multiline-comment-style
|
||||
/* a simple text STL file example:
|
||||
solid SimpleTriangle
|
||||
facet normal 0 0 1
|
||||
outer loop
|
||||
vertex 0 0 0
|
||||
vertex 1 0 0
|
||||
vertex 0 1 0
|
||||
endloop
|
||||
endfacet
|
||||
endsolid SimpleTriangle
|
||||
*/
|
||||
|
||||
export function newRenderPlugin3DViewer(): FileRenderPlugin {
|
||||
// Some extensions are text-based formats:
|
||||
// .3mf .amf .brep: XML
|
||||
// .fbx: XML or BINARY
|
||||
// .dae .gltf: JSON
|
||||
// .ifc, .igs, .iges, .stp, .step are: TEXT
|
||||
// .stl .ply: TEXT or BINARY
|
||||
// .obj .off .wrl: TEXT
|
||||
// So we need to be able to render when the file is recognized as plaintext file by backend.
|
||||
//
|
||||
// It needs more logic to make it overall right (render a text 3D model automatically):
|
||||
// we need to distinguish the ambiguous filename extensions.
|
||||
// For example: "*.obj, *.off, *.step" might be or not be a 3D model file.
|
||||
// So when it is a text file, we can't assume that "we only render it by 3D plugin",
|
||||
// otherwise the end users would be impossible to view its real content when the file is not a 3D model.
|
||||
const SUPPORTED_EXTENSIONS = [
|
||||
'.3dm', '.3ds', '.3mf', '.amf', '.bim', '.brep',
|
||||
'.dae', '.fbx', '.fcstd', '.glb', '.gltf',
|
||||
'.ifc', '.igs', '.iges', '.stp', '.step',
|
||||
'.stl', '.obj', '.off', '.ply', '.wrl',
|
||||
];
|
||||
|
||||
return {
|
||||
name: '3d-model-viewer',
|
||||
|
||||
canHandle(filename: string, _mimeType: string): boolean {
|
||||
const ext = extname(filename).toLowerCase();
|
||||
return SUPPORTED_EXTENSIONS.includes(ext);
|
||||
},
|
||||
|
||||
async render(container: HTMLElement, fileUrl: string): Promise<void> {
|
||||
// TODO: height and/or max-height?
|
||||
const OV = await import(/* webpackChunkName: "online-3d-viewer" */'online-3d-viewer');
|
||||
const viewer = new OV.EmbeddedViewer(container, {
|
||||
backgroundColor: new OV.RGBAColor(59, 68, 76, 0),
|
||||
defaultColor: new OV.RGBColor(65, 131, 196),
|
||||
edgeSettings: new OV.EdgeSettings(false, new OV.RGBColor(0, 0, 0), 1),
|
||||
});
|
||||
viewer.LoadModelFromUrlList([fileUrl]);
|
||||
},
|
||||
};
|
||||
}
|
20
web_src/js/render/plugins/pdf-viewer.ts
Normal file
20
web_src/js/render/plugins/pdf-viewer.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import type {FileRenderPlugin} from '../plugin.ts';
|
||||
|
||||
export function newRenderPluginPdfViewer(): FileRenderPlugin {
|
||||
return {
|
||||
name: 'pdf-viewer',
|
||||
|
||||
canHandle(filename: string, _mimeType: string): boolean {
|
||||
return filename.toLowerCase().endsWith('.pdf');
|
||||
},
|
||||
|
||||
async render(container: HTMLElement, fileUrl: string): Promise<void> {
|
||||
const PDFObject = await import(/* webpackChunkName: "pdfobject" */'pdfobject');
|
||||
// TODO: the PDFObject library does not support dynamic height adjustment,
|
||||
container.style.height = `${window.innerHeight - 100}px`;
|
||||
if (!PDFObject.default.embed(fileUrl, container)) {
|
||||
throw new Error('Unable to render the PDF file');
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user