mirror of
https://github.com/go-gitea/gitea.git
synced 2025-08-13 00:03:31 -04:00
Compare commits
9 Commits
d5b2bf9044
...
8030614386
Author | SHA1 | Date | |
---|---|---|---|
|
8030614386 | ||
|
5930ab5fdf | ||
|
9a0652f0b2 | ||
|
4f1065030f | ||
|
cd9a13ebb4 | ||
|
023a048f52 | ||
|
c090f87a8d | ||
|
b6fc2cdf82 | ||
|
2ee72d011f |
@ -31,7 +31,7 @@ vscode:
|
|||||||
- golang.go
|
- golang.go
|
||||||
- stylelint.vscode-stylelint
|
- stylelint.vscode-stylelint
|
||||||
- DavidAnson.vscode-markdownlint
|
- DavidAnson.vscode-markdownlint
|
||||||
- johnsoncodehk.volar
|
- Vue.volar
|
||||||
- ms-azuretools.vscode-docker
|
- ms-azuretools.vscode-docker
|
||||||
- zixuanchen.vitest-explorer
|
- zixuanchen.vitest-explorer
|
||||||
- alexcvzz.vscode-sqlite
|
- alexcvzz.vscode-sqlite
|
||||||
|
20
docs/content/doc/usage/profile-readme.en-us.md
Normal file
20
docs/content/doc/usage/profile-readme.en-us.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
date: "2023-03-02T21:00:00+05:00"
|
||||||
|
title: "Usage: Gitea Profile READMEs"
|
||||||
|
slug: "profile-readme"
|
||||||
|
weight: 12
|
||||||
|
toc: false
|
||||||
|
draft: false
|
||||||
|
menu:
|
||||||
|
sidebar:
|
||||||
|
parent: "usage"
|
||||||
|
name: "Gitea Profile READMEs"
|
||||||
|
weight: 12
|
||||||
|
identifier: "profile-readme"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Gitea Profile READMEs
|
||||||
|
|
||||||
|
To display a markdown file in your Gitea profile page, simply make a repository named ".profile" and edit the README.md file inside. Gitea will automatically pull this file in and display it above your repositories.
|
||||||
|
|
||||||
|
Note. You are welcome to make this repository private. Doing so will hide your source files from public viewing and allow you to privitize certain files. However, the README.md file will be the only file present on your profile. If you wish to have an entirely private .profile repository, remove or rename the README.md file.
|
@ -108,3 +108,31 @@
|
|||||||
is_prerelease: false
|
is_prerelease: false
|
||||||
is_tag: false
|
is_tag: false
|
||||||
created_unix: 946684803
|
created_unix: 946684803
|
||||||
|
|
||||||
|
- id: 9
|
||||||
|
repo_id: 57
|
||||||
|
publisher_id: 2
|
||||||
|
tag_name: "non-existing-target-branch"
|
||||||
|
lower_tag_name: "non-existing-target-branch"
|
||||||
|
target: "non-existing"
|
||||||
|
title: "non-existing-target-branch"
|
||||||
|
sha1: "cef06e48f2642cd0dc9597b4bea09f4b3f74aad6"
|
||||||
|
num_commits: 5
|
||||||
|
is_draft: false
|
||||||
|
is_prerelease: false
|
||||||
|
is_tag: false
|
||||||
|
created_unix: 946684803
|
||||||
|
|
||||||
|
- id: 10
|
||||||
|
repo_id: 57
|
||||||
|
publisher_id: 2
|
||||||
|
tag_name: "empty-target-branch"
|
||||||
|
lower_tag_name: "empty-target-branch"
|
||||||
|
target: ""
|
||||||
|
title: "empty-target-branch"
|
||||||
|
sha1: "cef06e48f2642cd0dc9597b4bea09f4b3f74aad6"
|
||||||
|
num_commits: 5
|
||||||
|
is_draft: false
|
||||||
|
is_prerelease: false
|
||||||
|
is_tag: false
|
||||||
|
created_unix: 946684803
|
||||||
|
@ -72,6 +72,7 @@ type Release struct {
|
|||||||
OriginalAuthorID int64 `xorm:"index"`
|
OriginalAuthorID int64 `xorm:"index"`
|
||||||
LowerTagName string
|
LowerTagName string
|
||||||
Target string
|
Target string
|
||||||
|
TargetBehind string `xorm:"-"` // to handle non-existing or empty target
|
||||||
Title string
|
Title string
|
||||||
Sha1 string `xorm:"VARCHAR(40)"`
|
Sha1 string `xorm:"VARCHAR(40)"`
|
||||||
NumCommits int64
|
NumCommits int64
|
||||||
|
@ -4,71 +4,20 @@
|
|||||||
package context
|
package context
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/httpcache"
|
"code.gitea.io/gitea/modules/httplib"
|
||||||
"code.gitea.io/gitea/modules/typesniffer"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServeHeaderOptions struct {
|
type ServeHeaderOptions httplib.ServeHeaderOptions
|
||||||
ContentType string // defaults to "application/octet-stream"
|
|
||||||
ContentTypeCharset string
|
|
||||||
ContentLength *int64
|
|
||||||
Disposition string // defaults to "attachment"
|
|
||||||
Filename string
|
|
||||||
CacheDuration time.Duration // defaults to 5 minutes
|
|
||||||
LastModified time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetServeHeaders sets necessary content serve headers
|
func (ctx *Context) SetServeHeaders(opt *ServeHeaderOptions) {
|
||||||
func (ctx *Context) SetServeHeaders(opts *ServeHeaderOptions) {
|
httplib.ServeSetHeaders(ctx.Resp, (*httplib.ServeHeaderOptions)(opt))
|
||||||
header := ctx.Resp.Header()
|
|
||||||
|
|
||||||
contentType := typesniffer.ApplicationOctetStream
|
|
||||||
if opts.ContentType != "" {
|
|
||||||
if opts.ContentTypeCharset != "" {
|
|
||||||
contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset)
|
|
||||||
} else {
|
|
||||||
contentType = opts.ContentType
|
|
||||||
}
|
|
||||||
}
|
|
||||||
header.Set("Content-Type", contentType)
|
|
||||||
header.Set("X-Content-Type-Options", "nosniff")
|
|
||||||
|
|
||||||
if opts.ContentLength != nil {
|
|
||||||
header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.Filename != "" {
|
|
||||||
disposition := opts.Disposition
|
|
||||||
if disposition == "" {
|
|
||||||
disposition = "attachment"
|
|
||||||
}
|
|
||||||
|
|
||||||
backslashEscapedName := strings.ReplaceAll(strings.ReplaceAll(opts.Filename, `\`, `\\`), `"`, `\"`) // \ -> \\, " -> \"
|
|
||||||
header.Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"; filename*=UTF-8''%s`, disposition, backslashEscapedName, url.PathEscape(opts.Filename)))
|
|
||||||
header.Set("Access-Control-Expose-Headers", "Content-Disposition")
|
|
||||||
}
|
|
||||||
|
|
||||||
duration := opts.CacheDuration
|
|
||||||
if duration == 0 {
|
|
||||||
duration = 5 * time.Minute
|
|
||||||
}
|
|
||||||
httpcache.SetCacheControlInHeader(header, duration)
|
|
||||||
|
|
||||||
if !opts.LastModified.IsZero() {
|
|
||||||
header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeContent serves content to http request
|
// ServeContent serves content to http request
|
||||||
func (ctx *Context) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) {
|
func (ctx *Context) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) {
|
||||||
ctx.SetServeHeaders(opts)
|
httplib.ServeSetHeaders(ctx.Resp, (*httplib.ServeHeaderOptions)(opts))
|
||||||
http.ServeContent(ctx.Resp, ctx.Req, opts.Filename, opts.LastModified, r)
|
http.ServeContent(ctx.Resp, ctx.Req, opts.Filename, opts.LastModified, r)
|
||||||
}
|
}
|
||||||
|
@ -7,32 +7,16 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/httplib"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockResponseWriter struct {
|
|
||||||
header http.Header
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockResponseWriter) Header() http.Header {
|
|
||||||
return m.header
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockResponseWriter) Write(bytes []byte) (int, error) {
|
|
||||||
panic("implement me")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockResponseWriter) WriteHeader(statusCode int) {
|
|
||||||
panic("implement me")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemoveSessionCookieHeader(t *testing.T) {
|
func TestRemoveSessionCookieHeader(t *testing.T) {
|
||||||
w := &mockResponseWriter{}
|
w := httplib.NewMockResponseWriter()
|
||||||
w.header = http.Header{}
|
w.Header().Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "foo"}).String())
|
||||||
w.header.Add("Set-Cookie", (&http.Cookie{Name: setting.SessionConfig.CookieName, Value: "foo"}).String())
|
w.Header().Add("Set-Cookie", (&http.Cookie{Name: "other", Value: "bar"}).String())
|
||||||
w.header.Add("Set-Cookie", (&http.Cookie{Name: "other", Value: "bar"}).String())
|
|
||||||
assert.Len(t, w.Header().Values("Set-Cookie"), 2)
|
assert.Len(t, w.Header().Values("Set-Cookie"), 2)
|
||||||
removeSessionCookieHeader(w)
|
removeSessionCookieHeader(w)
|
||||||
assert.Len(t, w.Header().Values("Set-Cookie"), 1)
|
assert.Len(t, w.Header().Values("Set-Cookie"), 1)
|
||||||
|
35
modules/httplib/mock.go
Normal file
35
modules/httplib/mock.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package httplib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockResponseWriter struct {
|
||||||
|
header http.Header
|
||||||
|
|
||||||
|
StatusCode int
|
||||||
|
BodyBuffer bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockResponseWriter) Header() http.Header {
|
||||||
|
return m.header
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockResponseWriter) Write(bytes []byte) (int, error) {
|
||||||
|
if m.StatusCode == 0 {
|
||||||
|
m.StatusCode = http.StatusOK
|
||||||
|
}
|
||||||
|
return m.BodyBuffer.Write(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockResponseWriter) WriteHeader(statusCode int) {
|
||||||
|
m.StatusCode = statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMockResponseWriter() *MockResponseWriter {
|
||||||
|
return &MockResponseWriter{header: http.Header{}}
|
||||||
|
}
|
225
modules/httplib/serve.go
Normal file
225
modules/httplib/serve.go
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package httplib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
charsetModule "code.gitea.io/gitea/modules/charset"
|
||||||
|
"code.gitea.io/gitea/modules/httpcache"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/typesniffer"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServeHeaderOptions struct {
|
||||||
|
ContentType string // defaults to "application/octet-stream"
|
||||||
|
ContentTypeCharset string
|
||||||
|
ContentLength *int64
|
||||||
|
Disposition string // defaults to "attachment"
|
||||||
|
Filename string
|
||||||
|
CacheDuration time.Duration // defaults to 5 minutes
|
||||||
|
LastModified time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeSetHeaders sets necessary content serve headers
|
||||||
|
func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) {
|
||||||
|
header := w.Header()
|
||||||
|
|
||||||
|
contentType := typesniffer.ApplicationOctetStream
|
||||||
|
if opts.ContentType != "" {
|
||||||
|
if opts.ContentTypeCharset != "" {
|
||||||
|
contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset)
|
||||||
|
} else {
|
||||||
|
contentType = opts.ContentType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
header.Set("Content-Type", contentType)
|
||||||
|
header.Set("X-Content-Type-Options", "nosniff")
|
||||||
|
|
||||||
|
if opts.ContentLength != nil {
|
||||||
|
header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Filename != "" {
|
||||||
|
disposition := opts.Disposition
|
||||||
|
if disposition == "" {
|
||||||
|
disposition = "attachment"
|
||||||
|
}
|
||||||
|
|
||||||
|
backslashEscapedName := strings.ReplaceAll(strings.ReplaceAll(opts.Filename, `\`, `\\`), `"`, `\"`) // \ -> \\, " -> \"
|
||||||
|
header.Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"; filename*=UTF-8''%s`, disposition, backslashEscapedName, url.PathEscape(opts.Filename)))
|
||||||
|
header.Set("Access-Control-Expose-Headers", "Content-Disposition")
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := opts.CacheDuration
|
||||||
|
if duration == 0 {
|
||||||
|
duration = 5 * time.Minute
|
||||||
|
}
|
||||||
|
httpcache.SetCacheControlInHeader(header, duration)
|
||||||
|
|
||||||
|
if !opts.LastModified.IsZero() {
|
||||||
|
header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeData download file from io.Reader
|
||||||
|
func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, filePath string, mineBuf []byte) {
|
||||||
|
// do not set "Content-Length", because the length could only be set by callers, and it needs to support range requests
|
||||||
|
opts := &ServeHeaderOptions{
|
||||||
|
Filename: path.Base(filePath),
|
||||||
|
}
|
||||||
|
|
||||||
|
sniffedType := typesniffer.DetectContentType(mineBuf)
|
||||||
|
|
||||||
|
// the "render" parameter came from year 2016: 638dd24c, it doesn't have clear meaning, so I think it could be removed later
|
||||||
|
isPlain := sniffedType.IsText() || r.FormValue("render") != ""
|
||||||
|
|
||||||
|
if setting.MimeTypeMap.Enabled {
|
||||||
|
fileExtension := strings.ToLower(filepath.Ext(filePath))
|
||||||
|
opts.ContentType = setting.MimeTypeMap.Map[fileExtension]
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.ContentType == "" {
|
||||||
|
if sniffedType.IsBrowsableBinaryType() {
|
||||||
|
opts.ContentType = sniffedType.GetMimeType()
|
||||||
|
} else if isPlain {
|
||||||
|
opts.ContentType = "text/plain"
|
||||||
|
} else {
|
||||||
|
opts.ContentType = typesniffer.ApplicationOctetStream
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isPlain {
|
||||||
|
charset, err := charsetModule.DetectEncoding(mineBuf)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Detect raw file %s charset failed: %v, using by default utf-8", filePath, err)
|
||||||
|
charset = "utf-8"
|
||||||
|
}
|
||||||
|
opts.ContentTypeCharset = strings.ToLower(charset)
|
||||||
|
}
|
||||||
|
|
||||||
|
isSVG := sniffedType.IsSvgImage()
|
||||||
|
|
||||||
|
// serve types that can present a security risk with CSP
|
||||||
|
if isSVG {
|
||||||
|
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
|
||||||
|
} else if sniffedType.IsPDF() {
|
||||||
|
// no sandbox attribute for pdf as it breaks rendering in at least safari. this
|
||||||
|
// should generally be safe as scripts inside PDF can not escape the PDF document
|
||||||
|
// see https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion
|
||||||
|
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.Disposition = "inline"
|
||||||
|
if isSVG && !setting.UI.SVG.Enabled {
|
||||||
|
opts.Disposition = "attachment"
|
||||||
|
}
|
||||||
|
|
||||||
|
ServeSetHeaders(w, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mimeDetectionBufferLen = 1024
|
||||||
|
|
||||||
|
func ServeContentByReader(r *http.Request, w http.ResponseWriter, filePath string, size int64, reader io.Reader) {
|
||||||
|
buf := make([]byte, mimeDetectionBufferLen)
|
||||||
|
n, err := util.ReadAtMost(reader, buf)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "serve content: unable to pre-read", http.StatusRequestedRangeNotSatisfiable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if n >= 0 {
|
||||||
|
buf = buf[:n]
|
||||||
|
}
|
||||||
|
setServeHeadersByFile(r, w, filePath, buf)
|
||||||
|
|
||||||
|
// reset the reader to the beginning
|
||||||
|
reader = io.MultiReader(bytes.NewReader(buf), reader)
|
||||||
|
|
||||||
|
rangeHeader := r.Header.Get("Range")
|
||||||
|
|
||||||
|
// if no size or no supported range, serve as 200 (complete response)
|
||||||
|
if size <= 0 || !strings.HasPrefix(rangeHeader, "bytes=") {
|
||||||
|
if size >= 0 {
|
||||||
|
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
|
||||||
|
}
|
||||||
|
_, _ = io.Copy(w, reader) // just like http.ServeContent, not necessary to handle the error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// do our best to support the minimal "Range" request (no support for multiple range: "Range: bytes=0-50, 100-150")
|
||||||
|
//
|
||||||
|
// GET /...
|
||||||
|
// Range: bytes=0-1023
|
||||||
|
//
|
||||||
|
// HTTP/1.1 206 Partial Content
|
||||||
|
// Content-Range: bytes 0-1023/146515
|
||||||
|
// Content-Length: 1024
|
||||||
|
|
||||||
|
_, rangeParts, _ := strings.Cut(rangeHeader, "=")
|
||||||
|
rangeBytesStart, rangeBytesEnd, found := strings.Cut(rangeParts, "-")
|
||||||
|
start, err := strconv.ParseInt(rangeBytesStart, 10, 64)
|
||||||
|
if start < 0 || start >= size {
|
||||||
|
err = errors.New("invalid start range")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusRequestedRangeNotSatisfiable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
end, err := strconv.ParseInt(rangeBytesEnd, 10, 64)
|
||||||
|
if rangeBytesEnd == "" && found {
|
||||||
|
err = nil
|
||||||
|
end = size - 1
|
||||||
|
}
|
||||||
|
if end >= size {
|
||||||
|
end = size - 1
|
||||||
|
}
|
||||||
|
if end < start {
|
||||||
|
err = errors.New("invalid end range")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
partialLength := end - start + 1
|
||||||
|
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, size))
|
||||||
|
w.Header().Set("Content-Length", strconv.FormatInt(partialLength, 10))
|
||||||
|
if _, err = io.CopyN(io.Discard, reader, start); err != nil {
|
||||||
|
http.Error(w, "serve content: unable to skip", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusPartialContent)
|
||||||
|
_, _ = io.CopyN(w, reader, partialLength) // just like http.ServeContent, not necessary to handle the error
|
||||||
|
}
|
||||||
|
|
||||||
|
func ServeContentByReadSeeker(r *http.Request, w http.ResponseWriter, filePath string, modTime time.Time, reader io.ReadSeeker) {
|
||||||
|
buf := make([]byte, mimeDetectionBufferLen)
|
||||||
|
n, err := util.ReadAtMost(reader, buf)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "serve content: unable to read", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err = reader.Seek(0, io.SeekStart); err != nil {
|
||||||
|
http.Error(w, "serve content: unable to seek", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if n >= 0 {
|
||||||
|
buf = buf[:n]
|
||||||
|
}
|
||||||
|
setServeHeadersByFile(r, w, filePath, buf)
|
||||||
|
http.ServeContent(w, r, path.Base(filePath), modTime, reader)
|
||||||
|
}
|
109
modules/httplib/serve_test.go
Normal file
109
modules/httplib/serve_test.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package httplib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServeContentByReader(t *testing.T) {
|
||||||
|
data := "0123456789abcdef"
|
||||||
|
|
||||||
|
test := func(t *testing.T, expectedStatusCode int, expectedContent string) {
|
||||||
|
_, rangeStr, _ := strings.Cut(t.Name(), "_range_")
|
||||||
|
r := &http.Request{Header: http.Header{}, Form: url.Values{}}
|
||||||
|
if rangeStr != "" {
|
||||||
|
r.Header.Set("Range", fmt.Sprintf("bytes=%s", rangeStr))
|
||||||
|
}
|
||||||
|
reader := strings.NewReader(data)
|
||||||
|
w := NewMockResponseWriter()
|
||||||
|
ServeContentByReader(r, w, "test", int64(len(data)), reader)
|
||||||
|
assert.Equal(t, expectedStatusCode, w.StatusCode)
|
||||||
|
if expectedStatusCode == http.StatusPartialContent || expectedStatusCode == http.StatusOK {
|
||||||
|
assert.Equal(t, fmt.Sprint(len(expectedContent)), w.Header().Get("Content-Length"))
|
||||||
|
assert.Equal(t, expectedContent, w.BodyBuffer.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("_range_", func(t *testing.T) {
|
||||||
|
test(t, http.StatusOK, data)
|
||||||
|
})
|
||||||
|
t.Run("_range_0-", func(t *testing.T) {
|
||||||
|
test(t, http.StatusPartialContent, data)
|
||||||
|
})
|
||||||
|
t.Run("_range_0-15", func(t *testing.T) {
|
||||||
|
test(t, http.StatusPartialContent, data)
|
||||||
|
})
|
||||||
|
t.Run("_range_1-", func(t *testing.T) {
|
||||||
|
test(t, http.StatusPartialContent, data[1:])
|
||||||
|
})
|
||||||
|
t.Run("_range_1-3", func(t *testing.T) {
|
||||||
|
test(t, http.StatusPartialContent, data[1:3+1])
|
||||||
|
})
|
||||||
|
t.Run("_range_16-", func(t *testing.T) {
|
||||||
|
test(t, http.StatusRequestedRangeNotSatisfiable, "")
|
||||||
|
})
|
||||||
|
t.Run("_range_1-99999", func(t *testing.T) {
|
||||||
|
test(t, http.StatusPartialContent, data[1:])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServeContentByReadSeeker(t *testing.T) {
|
||||||
|
data := "0123456789abcdef"
|
||||||
|
tmpFile := t.TempDir() + "/test"
|
||||||
|
err := os.WriteFile(tmpFile, []byte(data), 0o644)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
test := func(t *testing.T, expectedStatusCode int, expectedContent string) {
|
||||||
|
_, rangeStr, _ := strings.Cut(t.Name(), "_range_")
|
||||||
|
r := &http.Request{Header: http.Header{}, Form: url.Values{}}
|
||||||
|
if rangeStr != "" {
|
||||||
|
r.Header.Set("Range", fmt.Sprintf("bytes=%s", rangeStr))
|
||||||
|
}
|
||||||
|
|
||||||
|
seekReader, err := os.OpenFile(tmpFile, os.O_RDONLY, 0o644)
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer seekReader.Close()
|
||||||
|
|
||||||
|
w := NewMockResponseWriter()
|
||||||
|
ServeContentByReadSeeker(r, w, "test", time.Time{}, seekReader)
|
||||||
|
assert.Equal(t, expectedStatusCode, w.StatusCode)
|
||||||
|
if expectedStatusCode == http.StatusPartialContent || expectedStatusCode == http.StatusOK {
|
||||||
|
assert.Equal(t, fmt.Sprint(len(expectedContent)), w.Header().Get("Content-Length"))
|
||||||
|
assert.Equal(t, expectedContent, w.BodyBuffer.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("_range_", func(t *testing.T) {
|
||||||
|
test(t, http.StatusOK, data)
|
||||||
|
})
|
||||||
|
t.Run("_range_0-", func(t *testing.T) {
|
||||||
|
test(t, http.StatusPartialContent, data)
|
||||||
|
})
|
||||||
|
t.Run("_range_0-15", func(t *testing.T) {
|
||||||
|
test(t, http.StatusPartialContent, data)
|
||||||
|
})
|
||||||
|
t.Run("_range_1-", func(t *testing.T) {
|
||||||
|
test(t, http.StatusPartialContent, data[1:])
|
||||||
|
})
|
||||||
|
t.Run("_range_1-3", func(t *testing.T) {
|
||||||
|
test(t, http.StatusPartialContent, data[1:3+1])
|
||||||
|
})
|
||||||
|
t.Run("_range_16-", func(t *testing.T) {
|
||||||
|
test(t, http.StatusRequestedRangeNotSatisfiable, "")
|
||||||
|
})
|
||||||
|
t.Run("_range_1-99999", func(t *testing.T) {
|
||||||
|
test(t, http.StatusPartialContent, data[1:])
|
||||||
|
})
|
||||||
|
}
|
@ -18,9 +18,9 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrHashMismatch occurs if the content has does not match OID
|
// ErrHashMismatch occurs if the content has does not match OID
|
||||||
ErrHashMismatch = errors.New("Content hash does not match OID")
|
ErrHashMismatch = errors.New("content hash does not match OID")
|
||||||
// ErrSizeMismatch occurs if the content size does not match
|
// ErrSizeMismatch occurs if the content size does not match
|
||||||
ErrSizeMismatch = errors.New("Content size does not match")
|
ErrSizeMismatch = errors.New("content size does not match")
|
||||||
)
|
)
|
||||||
|
|
||||||
// ContentStore provides a simple file system based storage.
|
// ContentStore provides a simple file system based storage.
|
||||||
@ -105,7 +105,7 @@ func (s *ContentStore) Verify(pointer Pointer) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ReadMetaObject will read a git_model.LFSMetaObject and return a reader
|
// ReadMetaObject will read a git_model.LFSMetaObject and return a reader
|
||||||
func ReadMetaObject(pointer Pointer) (io.ReadCloser, error) {
|
func ReadMetaObject(pointer Pointer) (io.ReadSeekCloser, error) {
|
||||||
contentStore := NewContentStore()
|
contentStore := NewContentStore()
|
||||||
return contentStore.Get(pointer)
|
return contentStore.Get(pointer)
|
||||||
}
|
}
|
||||||
|
@ -249,10 +249,16 @@ type CreateBranchRepoOption struct {
|
|||||||
// unique: true
|
// unique: true
|
||||||
BranchName string `json:"new_branch_name" binding:"Required;GitRefName;MaxSize(100)"`
|
BranchName string `json:"new_branch_name" binding:"Required;GitRefName;MaxSize(100)"`
|
||||||
|
|
||||||
|
// Deprecated: true
|
||||||
// Name of the old branch to create from
|
// Name of the old branch to create from
|
||||||
//
|
//
|
||||||
// unique: true
|
// unique: true
|
||||||
OldBranchName string `json:"old_branch_name" binding:"GitRefName;MaxSize(100)"`
|
OldBranchName string `json:"old_branch_name" binding:"GitRefName;MaxSize(100)"`
|
||||||
|
|
||||||
|
// Name of the old branch/tag/commit to create from
|
||||||
|
//
|
||||||
|
// unique: true
|
||||||
|
OldRefName string `json:"old_ref_name" binding:"GitRefName;MaxSize(100)"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransferRepoOption options when transfer a repository's ownership
|
// TransferRepoOption options when transfer a repository's ownership
|
||||||
|
@ -569,6 +569,7 @@ starred = Starred Repositories
|
|||||||
watched = Watched Repositories
|
watched = Watched Repositories
|
||||||
code = Code
|
code = Code
|
||||||
projects = Projects
|
projects = Projects
|
||||||
|
overview = Overview
|
||||||
following = Following
|
following = Following
|
||||||
follow = Follow
|
follow = Follow
|
||||||
unfollow = Unfollow
|
unfollow = Unfollow
|
||||||
|
@ -173,11 +173,35 @@ func CreateBranch(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(opt.OldBranchName) == 0 {
|
var oldCommit *git.Commit
|
||||||
opt.OldBranchName = ctx.Repo.Repository.DefaultBranch
|
var err error
|
||||||
|
|
||||||
|
if len(opt.OldRefName) > 0 {
|
||||||
|
oldCommit, err = ctx.Repo.GitRepo.GetCommit(opt.OldRefName)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "GetCommit", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if len(opt.OldBranchName) > 0 { //nolint
|
||||||
|
if ctx.Repo.GitRepo.IsBranchExist(opt.OldBranchName) { //nolint
|
||||||
|
oldCommit, err = ctx.Repo.GitRepo.GetBranchCommit(opt.OldBranchName) //nolint
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "GetBranchCommit", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.Error(http.StatusNotFound, "", "The old branch does not exist")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
oldCommit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "GetBranchCommit", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := repo_service.CreateNewBranch(ctx, ctx.Doer, ctx.Repo.Repository, opt.OldBranchName, opt.BranchName)
|
err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, oldCommit.ID.String(), opt.BranchName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if models.IsErrBranchDoesNotExist(err) {
|
if models.IsErrBranchDoesNotExist(err) {
|
||||||
ctx.Error(http.StatusNotFound, "", "The old branch does not exist")
|
ctx.Error(http.StatusNotFound, "", "The old branch does not exist")
|
||||||
@ -189,7 +213,7 @@ func CreateBranch(ctx *context.APIContext) {
|
|||||||
} else if models.IsErrBranchNameConflict(err) {
|
} else if models.IsErrBranchNameConflict(err) {
|
||||||
ctx.Error(http.StatusConflict, "", "The branch with the same name already exists.")
|
ctx.Error(http.StatusConflict, "", "The branch with the same name already exists.")
|
||||||
} else {
|
} else {
|
||||||
ctx.Error(http.StatusInternalServerError, "CreateRepoBranch", err)
|
ctx.Error(http.StatusInternalServerError, "CreateNewBranchFromCommit", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,18 @@ func GetSingleCommit(ctx *context.APIContext) {
|
|||||||
// description: a git ref or commit sha
|
// description: a git ref or commit sha
|
||||||
// type: string
|
// type: string
|
||||||
// required: true
|
// required: true
|
||||||
|
// - name: stat
|
||||||
|
// in: query
|
||||||
|
// description: include diff stats for every commit (disable for speedup, default 'true')
|
||||||
|
// type: boolean
|
||||||
|
// - name: verification
|
||||||
|
// in: query
|
||||||
|
// description: include verification for every commit (disable for speedup, default 'true')
|
||||||
|
// type: boolean
|
||||||
|
// - name: files
|
||||||
|
// in: query
|
||||||
|
// description: include a list of affected files for every commit (disable for speedup, default 'true')
|
||||||
|
// type: boolean
|
||||||
// responses:
|
// responses:
|
||||||
// "200":
|
// "200":
|
||||||
// "$ref": "#/responses/Commit"
|
// "$ref": "#/responses/Commit"
|
||||||
@ -55,10 +67,11 @@ func GetSingleCommit(ctx *context.APIContext) {
|
|||||||
ctx.Error(http.StatusUnprocessableEntity, "no valid ref or sha", fmt.Sprintf("no valid ref or sha: %s", sha))
|
ctx.Error(http.StatusUnprocessableEntity, "no valid ref or sha", fmt.Sprintf("no valid ref or sha: %s", sha))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
getCommit(ctx, sha)
|
|
||||||
|
getCommit(ctx, sha, convert.ParseCommitOptions(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCommit(ctx *context.APIContext, identifier string) {
|
func getCommit(ctx *context.APIContext, identifier string, toCommitOpts convert.ToCommitOptions) {
|
||||||
commit, err := ctx.Repo.GitRepo.GetCommit(identifier)
|
commit, err := ctx.Repo.GitRepo.GetCommit(identifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if git.IsErrNotExist(err) {
|
if git.IsErrNotExist(err) {
|
||||||
@ -69,7 +82,7 @@ func getCommit(ctx *context.APIContext, identifier string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
json, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, commit, nil, convert.ToCommitOptions{Stat: true})
|
json, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, commit, nil, toCommitOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, "toCommit", err)
|
ctx.Error(http.StatusInternalServerError, "toCommit", err)
|
||||||
return
|
return
|
||||||
@ -240,24 +253,12 @@ func GetAllCommits(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pageCount := int(math.Ceil(float64(commitsCountTotal) / float64(listOptions.PageSize)))
|
pageCount := int(math.Ceil(float64(commitsCountTotal) / float64(listOptions.PageSize)))
|
||||||
|
|
||||||
userCache := make(map[string]*user_model.User)
|
userCache := make(map[string]*user_model.User)
|
||||||
|
|
||||||
apiCommits := make([]*api.Commit, len(commits))
|
apiCommits := make([]*api.Commit, len(commits))
|
||||||
|
|
||||||
stat := ctx.FormString("stat") == "" || ctx.FormBool("stat")
|
|
||||||
verification := ctx.FormString("verification") == "" || ctx.FormBool("verification")
|
|
||||||
files := ctx.FormString("files") == "" || ctx.FormBool("files")
|
|
||||||
|
|
||||||
for i, commit := range commits {
|
for i, commit := range commits {
|
||||||
// Create json struct
|
// Create json struct
|
||||||
apiCommits[i], err = convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, commit, userCache,
|
apiCommits[i], err = convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, commit, userCache, convert.ParseCommitOptions(ctx))
|
||||||
convert.ToCommitOptions{
|
|
||||||
Stat: stat,
|
|
||||||
Verification: verification,
|
|
||||||
Files: files,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, "toCommit", err)
|
ctx.Error(http.StatusInternalServerError, "toCommit", err)
|
||||||
return
|
return
|
||||||
|
@ -150,6 +150,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: code from #19689, what if the file is large ... OOM ...
|
||||||
buf, err := io.ReadAll(dataRc)
|
buf, err := io.ReadAll(dataRc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = dataRc.Close()
|
_ = dataRc.Close()
|
||||||
@ -164,7 +165,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
|
|||||||
// Check if the blob represents a pointer
|
// Check if the blob represents a pointer
|
||||||
pointer, _ := lfs.ReadPointer(bytes.NewReader(buf))
|
pointer, _ := lfs.ReadPointer(bytes.NewReader(buf))
|
||||||
|
|
||||||
// if its not a pointer just serve the data directly
|
// if it's not a pointer, just serve the data directly
|
||||||
if !pointer.IsValid() {
|
if !pointer.IsValid() {
|
||||||
// First handle caching for the blob
|
// First handle caching for the blob
|
||||||
if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
|
if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
|
||||||
@ -172,25 +173,21 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OK not cached - serve!
|
// OK not cached - serve!
|
||||||
if err := common.ServeData(ctx.Context, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf)); err != nil {
|
common.ServeContentByReader(ctx.Context, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf))
|
||||||
ctx.ServerError("ServeBlob", err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now check if there is a meta object for this pointer
|
// Now check if there is a MetaObject for this pointer
|
||||||
meta, err := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, pointer.Oid)
|
meta, err := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, pointer.Oid)
|
||||||
|
|
||||||
// If there isn't one just serve the data directly
|
// If there isn't one, just serve the data directly
|
||||||
if err == git_model.ErrLFSObjectNotExist {
|
if err == git_model.ErrLFSObjectNotExist {
|
||||||
// Handle caching for the blob SHA (not the LFS object OID)
|
// Handle caching for the blob SHA (not the LFS object OID)
|
||||||
if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
|
if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := common.ServeData(ctx.Context, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf)); err != nil {
|
common.ServeContentByReader(ctx.Context, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf))
|
||||||
ctx.ServerError("ServeBlob", err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
ctx.ServerError("GetLFSMetaObjectByOid", err)
|
ctx.ServerError("GetLFSMetaObjectByOid", err)
|
||||||
@ -218,9 +215,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
defer lfsDataRc.Close()
|
defer lfsDataRc.Close()
|
||||||
|
|
||||||
if err := common.ServeData(ctx.Context, ctx.Repo.TreePath, meta.Size, lfsDataRc); err != nil {
|
common.ServeContentByReadSeeker(ctx.Context, ctx.Repo.TreePath, lastModified, lfsDataRc)
|
||||||
ctx.ServerError("ServeData", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified time.Time) {
|
func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified time.Time) {
|
||||||
|
@ -1,116 +0,0 @@
|
|||||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
charsetModule "code.gitea.io/gitea/modules/charset"
|
|
||||||
"code.gitea.io/gitea/modules/context"
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
|
||||||
"code.gitea.io/gitea/modules/httpcache"
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
"code.gitea.io/gitea/modules/typesniffer"
|
|
||||||
"code.gitea.io/gitea/modules/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ServeBlob download a git.Blob
|
|
||||||
func ServeBlob(ctx *context.Context, blob *git.Blob, lastModified time.Time) error {
|
|
||||||
if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
dataRc, err := blob.DataAsync()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err = dataRc.Close(); err != nil {
|
|
||||||
log.Error("ServeBlob: Close: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return ServeData(ctx, ctx.Repo.TreePath, blob.Size(), dataRc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeData download file from io.Reader
|
|
||||||
func ServeData(ctx *context.Context, filePath string, size int64, reader io.Reader) error {
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
n, err := util.ReadAtMost(reader, buf)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if n >= 0 {
|
|
||||||
buf = buf[:n]
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := &context.ServeHeaderOptions{
|
|
||||||
Filename: path.Base(filePath),
|
|
||||||
}
|
|
||||||
|
|
||||||
if size >= 0 {
|
|
||||||
opts.ContentLength = &size
|
|
||||||
} else {
|
|
||||||
log.Error("ServeData called to serve data: %s with size < 0: %d", filePath, size)
|
|
||||||
}
|
|
||||||
|
|
||||||
sniffedType := typesniffer.DetectContentType(buf)
|
|
||||||
isPlain := sniffedType.IsText() || ctx.FormBool("render")
|
|
||||||
|
|
||||||
if setting.MimeTypeMap.Enabled {
|
|
||||||
fileExtension := strings.ToLower(filepath.Ext(filePath))
|
|
||||||
opts.ContentType = setting.MimeTypeMap.Map[fileExtension]
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.ContentType == "" {
|
|
||||||
if sniffedType.IsBrowsableBinaryType() {
|
|
||||||
opts.ContentType = sniffedType.GetMimeType()
|
|
||||||
} else if isPlain {
|
|
||||||
opts.ContentType = "text/plain"
|
|
||||||
} else {
|
|
||||||
opts.ContentType = typesniffer.ApplicationOctetStream
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if isPlain {
|
|
||||||
var charset string
|
|
||||||
charset, err = charsetModule.DetectEncoding(buf)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Detect raw file %s charset failed: %v, using by default utf-8", filePath, err)
|
|
||||||
charset = "utf-8"
|
|
||||||
}
|
|
||||||
opts.ContentTypeCharset = strings.ToLower(charset)
|
|
||||||
}
|
|
||||||
|
|
||||||
isSVG := sniffedType.IsSvgImage()
|
|
||||||
|
|
||||||
// serve types that can present a security risk with CSP
|
|
||||||
if isSVG {
|
|
||||||
ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
|
|
||||||
} else if sniffedType.IsPDF() {
|
|
||||||
// no sandbox attribute for pdf as it breaks rendering in at least safari. this
|
|
||||||
// should generally be safe as scripts inside PDF can not escape the PDF document
|
|
||||||
// see https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion
|
|
||||||
ctx.Resp.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
|
|
||||||
}
|
|
||||||
|
|
||||||
opts.Disposition = "inline"
|
|
||||||
if isSVG && !setting.UI.SVG.Enabled {
|
|
||||||
opts.Disposition = "attachment"
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.SetServeHeaders(opts)
|
|
||||||
|
|
||||||
_, err = ctx.Resp.Write(buf)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = io.Copy(ctx.Resp, reader)
|
|
||||||
return err
|
|
||||||
}
|
|
43
routers/common/serve.go
Normal file
43
routers/common/serve.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/httpcache"
|
||||||
|
"code.gitea.io/gitea/modules/httplib"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServeBlob download a git.Blob
|
||||||
|
func ServeBlob(ctx *context.Context, blob *git.Blob, lastModified time.Time) error {
|
||||||
|
if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dataRc, err := blob.DataAsync()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err = dataRc.Close(); err != nil {
|
||||||
|
log.Error("ServeBlob: Close: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
httplib.ServeContentByReader(ctx.Req, ctx.Resp, ctx.Repo.TreePath, blob.Size(), dataRc)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ServeContentByReader(ctx *context.Context, filePath string, size int64, reader io.Reader) {
|
||||||
|
httplib.ServeContentByReader(ctx.Req, ctx.Resp, filePath, size, reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ServeContentByReadSeeker(ctx *context.Context, filePath string, modTime time.Time, reader io.ReadSeeker) {
|
||||||
|
httplib.ServeContentByReadSeeker(ctx.Req, ctx.Resp, filePath, modTime, reader)
|
||||||
|
}
|
@ -153,10 +153,7 @@ func ServeAttachment(ctx *context.Context, uuid string) {
|
|||||||
}
|
}
|
||||||
defer fr.Close()
|
defer fr.Close()
|
||||||
|
|
||||||
if err = common.ServeData(ctx, attach.Name, attach.Size, fr); err != nil {
|
common.ServeContentByReadSeeker(ctx, attach.Name, attach.CreatedUnix.AsTime(), fr)
|
||||||
ctx.ServerError("ServeData", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAttachment serve attachments
|
// GetAttachment serve attachments
|
||||||
|
@ -71,7 +71,8 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified time.Time
|
|||||||
log.Error("ServeBlobOrLFS: Close: %v", err)
|
log.Error("ServeBlobOrLFS: Close: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
return common.ServeData(ctx, ctx.Repo.TreePath, meta.Size, lfsDataRc)
|
common.ServeContentByReadSeeker(ctx, ctx.Repo.TreePath, lastModified, lfsDataRc)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
if err = dataRc.Close(); err != nil {
|
if err = dataRc.Close(); err != nil {
|
||||||
log.Error("ServeBlobOrLFS: Close: %v", err)
|
log.Error("ServeBlobOrLFS: Close: %v", err)
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@ -16,6 +17,7 @@ import (
|
|||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
"code.gitea.io/gitea/modules/markup/markdown"
|
"code.gitea.io/gitea/modules/markup/markdown"
|
||||||
@ -36,24 +38,32 @@ const (
|
|||||||
|
|
||||||
// calReleaseNumCommitsBehind calculates given release has how many commits behind release target.
|
// calReleaseNumCommitsBehind calculates given release has how many commits behind release target.
|
||||||
func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *repo_model.Release, countCache map[string]int64) error {
|
func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *repo_model.Release, countCache map[string]int64) error {
|
||||||
// Get count if not exists
|
target := release.Target
|
||||||
if _, ok := countCache[release.Target]; !ok {
|
if target == "" {
|
||||||
// short-circuit for the default branch
|
target = repoCtx.Repository.DefaultBranch
|
||||||
if repoCtx.Repository.DefaultBranch == release.Target || repoCtx.GitRepo.IsBranchExist(release.Target) {
|
}
|
||||||
commit, err := repoCtx.GitRepo.GetBranchCommit(release.Target)
|
// Get count if not cached
|
||||||
if err != nil {
|
if _, ok := countCache[target]; !ok {
|
||||||
|
commit, err := repoCtx.GitRepo.GetBranchCommit(target)
|
||||||
|
if err != nil {
|
||||||
|
var errNotExist git.ErrNotExist
|
||||||
|
if target == repoCtx.Repository.DefaultBranch || !errors.As(err, &errNotExist) {
|
||||||
return fmt.Errorf("GetBranchCommit: %w", err)
|
return fmt.Errorf("GetBranchCommit: %w", err)
|
||||||
}
|
}
|
||||||
countCache[release.Target], err = commit.CommitsCount()
|
// fallback to default branch
|
||||||
|
target = repoCtx.Repository.DefaultBranch
|
||||||
|
commit, err = repoCtx.GitRepo.GetBranchCommit(target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("CommitsCount: %w", err)
|
return fmt.Errorf("GetBranchCommit(DefaultBranch): %w", err)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
// Use NumCommits of the newest release on that target
|
countCache[target], err = commit.CommitsCount()
|
||||||
countCache[release.Target] = release.NumCommits
|
if err != nil {
|
||||||
|
return fmt.Errorf("CommitsCount: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
release.NumCommitsBehind = countCache[release.Target] - release.NumCommits
|
release.NumCommitsBehind = countCache[target] - release.NumCommits
|
||||||
|
release.TargetBehind = target
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,8 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/test"
|
"code.gitea.io/gitea/modules/test"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewReleasePost(t *testing.T) {
|
func TestNewReleasePost(t *testing.T) {
|
||||||
@ -62,3 +64,48 @@ func TestNewReleasePost(t *testing.T) {
|
|||||||
ctx.Repo.GitRepo.Close()
|
ctx.Repo.GitRepo.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewReleasesList(t *testing.T) {
|
||||||
|
unittest.PrepareTestEnv(t)
|
||||||
|
ctx := test.MockContext(t, "user2/repo-release/releases")
|
||||||
|
test.LoadUser(t, ctx, 2)
|
||||||
|
test.LoadRepo(t, ctx, 57)
|
||||||
|
test.LoadGitRepo(t, ctx)
|
||||||
|
t.Cleanup(func() { ctx.Repo.GitRepo.Close() })
|
||||||
|
|
||||||
|
Releases(ctx)
|
||||||
|
releases := ctx.Data["Releases"].([]*repo_model.Release)
|
||||||
|
type computedFields struct {
|
||||||
|
NumCommitsBehind int64
|
||||||
|
TargetBehind string
|
||||||
|
}
|
||||||
|
expectedComputation := map[string]computedFields{
|
||||||
|
"v1.0": {
|
||||||
|
NumCommitsBehind: 3,
|
||||||
|
TargetBehind: "main",
|
||||||
|
},
|
||||||
|
"v1.1": {
|
||||||
|
NumCommitsBehind: 1,
|
||||||
|
TargetBehind: "main",
|
||||||
|
},
|
||||||
|
"v2.0": {
|
||||||
|
NumCommitsBehind: 0,
|
||||||
|
TargetBehind: "main",
|
||||||
|
},
|
||||||
|
"non-existing-target-branch": {
|
||||||
|
NumCommitsBehind: 1,
|
||||||
|
TargetBehind: "main",
|
||||||
|
},
|
||||||
|
"empty-target-branch": {
|
||||||
|
NumCommitsBehind: 1,
|
||||||
|
TargetBehind: "main",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, r := range releases {
|
||||||
|
actual := computedFields{
|
||||||
|
NumCommitsBehind: r.NumCommitsBehind,
|
||||||
|
TargetBehind: r.TargetBehind,
|
||||||
|
}
|
||||||
|
assert.Equal(t, expectedComputation[r.TagName], actual, "wrong computed fields for %s: %#v", r.TagName, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,7 +4,9 @@
|
|||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -13,4 +15,24 @@ func RenderUserHeader(ctx *context.Context) {
|
|||||||
ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled
|
ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled
|
||||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||||
ctx.Data["ContextUser"] = ctx.ContextUser
|
ctx.Data["ContextUser"] = ctx.ContextUser
|
||||||
|
tab := ctx.FormString("tab")
|
||||||
|
ctx.Data["TabName"] = tab
|
||||||
|
repo, err := repo_model.GetRepositoryByName(ctx.ContextUser.ID, ".profile")
|
||||||
|
if err == nil && !repo.IsEmpty {
|
||||||
|
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("OpenRepository", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer gitRepo.Close()
|
||||||
|
commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetBranchCommit", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
blob, err := commit.GetBlobByPath("README.md")
|
||||||
|
if err == nil && blob != nil {
|
||||||
|
ctx.Data["ProfileReadme"] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
"code.gitea.io/gitea/modules/markup/markdown"
|
"code.gitea.io/gitea/modules/markup/markdown"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
@ -91,6 +92,38 @@ func Profile(ctx *context.Context) {
|
|||||||
ctx.Data["RenderedDescription"] = content
|
ctx.Data["RenderedDescription"] = content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
repo, err := repo_model.GetRepositoryByName(ctx.ContextUser.ID, ".profile")
|
||||||
|
if err == nil && !repo.IsEmpty {
|
||||||
|
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("OpenRepository", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer gitRepo.Close()
|
||||||
|
commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetBranchCommit", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
blob, err := commit.GetBlobByPath("README.md")
|
||||||
|
if err == nil {
|
||||||
|
bytes, err := blob.GetBlobContent()
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetBlobContent", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
profileContent, err := markdown.RenderString(&markup.RenderContext{
|
||||||
|
Ctx: ctx,
|
||||||
|
GitRepo: gitRepo,
|
||||||
|
}, bytes)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("RenderString", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["ProfileReadme"] = profileContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
|
showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
|
||||||
|
|
||||||
orgs, err := organization.FindOrgs(organization.FindOrgOptions{
|
orgs, err := organization.FindOrgs(organization.FindOrgOptions{
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
ctx "code.gitea.io/gitea/modules/context"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
@ -78,6 +79,14 @@ type ToCommitOptions struct {
|
|||||||
Files bool
|
Files bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ParseCommitOptions(ctx *ctx.APIContext) ToCommitOptions {
|
||||||
|
return ToCommitOptions{
|
||||||
|
Stat: ctx.FormString("stat") == "" || ctx.FormBool("stat"),
|
||||||
|
Files: ctx.FormString("files") == "" || ctx.FormBool("files"),
|
||||||
|
Verification: ctx.FormString("verification") == "" || ctx.FormBool("verification"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ToCommit convert a git.Commit to api.Commit
|
// ToCommit convert a git.Commit to api.Commit
|
||||||
func ToCommit(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, commit *git.Commit, userCache map[string]*user_model.User, opts ToCommitOptions) (*api.Commit, error) {
|
func ToCommit(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, commit *git.Commit, userCache map[string]*user_model.User, opts ToCommitOptions) (*api.Commit, error) {
|
||||||
var apiAuthor, apiCommitter *api.User
|
var apiAuthor, apiCommitter *api.User
|
||||||
|
@ -73,7 +73,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="ui container fluid padded" id="project-board">
|
<div class="ui container fluid padded" id="project-board">
|
||||||
|
|
||||||
<div class="board">
|
<div class="board {{if .CanWriteProjects}}sortable{{end}}">
|
||||||
{{range $board := .Boards}}
|
{{range $board := .Boards}}
|
||||||
|
|
||||||
<div class="ui segment board-column" style="background: {{.Color}} !important;" data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}">
|
<div class="ui segment board-column" style="background: {{.Color}} !important;" data-id="{{.ID}}" data-sorting="{{.Sorting}}" data-url="{{$.Link}}/{{.ID}}">
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
{{range .Runs}}
|
{{range .Runs}}
|
||||||
<li class="item gt-df gt-py-3 gt-ab">
|
<li class="item gt-df gt-py-3 gt-ab">
|
||||||
<div class="issue-item-left gt-df gt-mr-2">
|
<div class="issue-item-left gt-df gt-mr-2">
|
||||||
{{template "repo/actions/status" (dict "status" .Status.String)}}
|
{{template "repo/actions/status" (dict "status" .Status.String "locale" $.locale)}}
|
||||||
</div>
|
</div>
|
||||||
<div class="issue-item-main action-item-main gt-f1 gt-fc gt-df gt-mr-3">
|
<div class="issue-item-main action-item-main gt-f1 gt-fc gt-df gt-mr-3">
|
||||||
<div class="issue-item-top-row">
|
<div class="issue-item-top-row">
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
{{- $className = .className -}}
|
{{- $className = .className -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
|
<span data-tooltip-content="{{.locale.Tr (printf "actions.status.%s" .status)}}">
|
||||||
{{if eq .status "success"}}
|
{{if eq .status "success"}}
|
||||||
{{svg "octicon-check-circle-fill" $size (printf "text green %s" $className)}}
|
{{svg "octicon-check-circle-fill" $size (printf "text green %s" $className)}}
|
||||||
{{else if eq .status "skipped"}}
|
{{else if eq .status "skipped"}}
|
||||||
@ -24,3 +25,4 @@
|
|||||||
{{else}}
|
{{else}}
|
||||||
{{svg "octicon-x-circle-fill" $size (printf "text red %s" $className)}}
|
{{svg "octicon-x-circle-fill" $size (printf "text red %s" $className)}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
</span>
|
||||||
|
@ -9,6 +9,14 @@
|
|||||||
data-locale-approve="{{.locale.Tr "repo.diff.review.approve"}}"
|
data-locale-approve="{{.locale.Tr "repo.diff.review.approve"}}"
|
||||||
data-locale-cancel="{{.locale.Tr "cancel"}}"
|
data-locale-cancel="{{.locale.Tr "cancel"}}"
|
||||||
data-locale-rerun="{{.locale.Tr "rerun"}}"
|
data-locale-rerun="{{.locale.Tr "rerun"}}"
|
||||||
|
data-locale-status-unknown="{{.locale.Tr "actions.status.unknown"}}"
|
||||||
|
data-locale-status-waiting="{{.locale.Tr "actions.status.waiting"}}"
|
||||||
|
data-locale-status-running="{{.locale.Tr "actions.status.running"}}"
|
||||||
|
data-locale-status-success="{{.locale.Tr "actions.status.success"}}"
|
||||||
|
data-locale-status-failure="{{.locale.Tr "actions.status.failure"}}"
|
||||||
|
data-locale-status-cancelled="{{.locale.Tr "actions.status.cancelled"}}"
|
||||||
|
data-locale-status-skipped="{{.locale.Tr "actions.status.skipped"}}"
|
||||||
|
data-locale-status-blocked="{{.locale.Tr "actions.status.blocked"}}"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{{if .DiffNotAvailable}}
|
{{if .DiffNotAvailable}}
|
||||||
<div class="diff-detail-box diff-box sticky">
|
<div>
|
||||||
<div>
|
<div class="diff-detail-box diff-box sticky">
|
||||||
<div class="ui right">
|
<div class="ui right">
|
||||||
{{template "repo/diff/whitespace_dropdown" .}}
|
{{template "repo/diff/whitespace_dropdown" .}}
|
||||||
{{template "repo/diff/options_dropdown" .}}
|
{{template "repo/diff/options_dropdown" .}}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
{{template "base/head" .}}
|
{{template "base/head" .}}
|
||||||
<div role="main" aria-label="{{.Title}}" class="page-content repository diff {{if .PageIsComparePull}}compare pull{{end}}">
|
<div role="main" aria-label="{{.Title}}" class="page-content repository diff {{if .PageIsComparePull}}compare pull{{end}}">
|
||||||
{{template "repo/header" .}}
|
{{template "repo/header" .}}
|
||||||
<div class="ui container fluid padded">
|
{{$showDiffBox := false}}
|
||||||
|
<div class="ui container">
|
||||||
<h2 class="ui header">
|
<h2 class="ui header">
|
||||||
{{if and $.PageIsComparePull $.IsSigned (not .Repository.IsArchived)}}
|
{{if and $.PageIsComparePull $.IsSigned (not .Repository.IsArchived)}}
|
||||||
{{.locale.Tr "repo.pulls.compare_changes"}}
|
{{.locale.Tr "repo.pulls.compare_changes"}}
|
||||||
@ -34,11 +34,6 @@
|
|||||||
{{- if eq $.HeadRepo.OwnerName .RootRepo.OwnerName -}}
|
{{- if eq $.HeadRepo.OwnerName .RootRepo.OwnerName -}}
|
||||||
{{- $HeadCompareName = printf "%s/%s" $.HeadRepo.OwnerName $.HeadRepo.Name -}}
|
{{- $HeadCompareName = printf "%s/%s" $.HeadRepo.OwnerName $.HeadRepo.Name -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- if .OwnForkRepo -}}
|
|
||||||
{{- if eq $.OwnForkRepo.OwnerName .RootRepo.OwnerName -}}
|
|
||||||
{{- $OwnForkRepoCompareName = printf "%s/%s" $.OwnForkRepo.OwnerName $.OwnForkRepo.Name -}}
|
|
||||||
{{- end -}}
|
|
||||||
{{- end -}}
|
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
<div class="ui segment choose branch">
|
<div class="ui segment choose branch">
|
||||||
<a href="{{$.HeadRepo.Link}}/compare/{{PathEscapeSegments $.HeadBranch}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.BaseName}}/{{PathEscape $.Repository.Name}}:{{end}}{{PathEscapeSegments $.BaseBranch}}" title="{{.locale.Tr "repo.pulls.switch_head_and_base"}}">{{svg "octicon-git-compare"}}</a>
|
<a href="{{$.HeadRepo.Link}}/compare/{{PathEscapeSegments $.HeadBranch}}{{$.CompareSeparator}}{{if not $.PullRequestCtx.SameRepo}}{{PathEscape $.BaseName}}/{{PathEscape $.Repository.Name}}:{{end}}{{PathEscapeSegments $.BaseBranch}}" title="{{.locale.Tr "repo.pulls.switch_head_and_base"}}">{{svg "octicon-git-compare"}}</a>
|
||||||
@ -203,14 +198,15 @@
|
|||||||
<span class="index">#{{.PullRequest.Issue.Index}}</span>
|
<span class="index">#{{.PullRequest.Issue.Index}}</span>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="four wide right middle aligned column">
|
<div class="four wide column middle aligned text right">
|
||||||
{{- if .PullRequest.HasMerged -}}
|
{{- if .PullRequest.HasMerged -}}
|
||||||
<a href="{{Escape $.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button purple show-form">{{svg "octicon-git-merge" 16}} {{.locale.Tr "repo.pulls.view"}}</a>
|
<a href="{{Escape $.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button purple show-form">{{svg "octicon-git-merge" 16}} {{.locale.Tr "repo.pulls.view"}}</a>
|
||||||
{{else if .Issue.IsClosed}}
|
{{else if .Issue.IsClosed}}
|
||||||
<a href="{{Escape $.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button red show-form">{{svg "octicon-issue-closed" 16}} {{.locale.Tr "repo.pulls.view"}}</a>
|
<a href="{{Escape $.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button red show-form">{{svg "octicon-issue-closed" 16}} {{.locale.Tr "repo.pulls.view"}}</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
<a href="{{Escape $.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button green show-form">{{svg "octicon-git-pull-request" 16}} {{.locale.Tr "repo.pulls.view"}}</a>
|
<a href="{{Escape $.RepoLink}}/pulls/{{.PullRequest.Issue.Index}}" class="ui button green show-form">{{svg "octicon-git-pull-request" 16}} {{.locale.Tr "repo.pulls.view"}}</a>
|
||||||
{{end}}</div>
|
{{end}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{if and $.IsSigned (not .Repository.IsArchived)}}
|
{{if and $.IsSigned (not .Repository.IsArchived)}}
|
||||||
@ -231,13 +227,20 @@
|
|||||||
{{template "repo/issue/new_form" .}}
|
{{template "repo/issue/new_form" .}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{template "repo/commits_table" .}}
|
{{$showDiffBox = true}}
|
||||||
{{template "repo/diff/box" .}}
|
|
||||||
{{end}}
|
{{end}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{template "repo/commits_table" .}}
|
{{$showDiffBox = true}}
|
||||||
{{template "repo/diff/box" .}}
|
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{if $showDiffBox}}
|
||||||
|
<div class="ui container">
|
||||||
|
{{template "repo/commits_table" .}}
|
||||||
|
</div>
|
||||||
|
<div class="ui container fluid padded">
|
||||||
|
{{template "repo/diff/box" .}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{template "base/footer" .}}
|
{{template "base/footer" .}}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<div role="main" aria-label="{{.Title}}" class="page-content repository view issue pull files diff">
|
<div role="main" aria-label="{{.Title}}" class="page-content repository view issue pull files diff">
|
||||||
{{template "repo/header" .}}
|
{{template "repo/header" .}}
|
||||||
<div class="ui container">
|
<div class="ui container fluid padded">
|
||||||
{{template "repo/issue/view_title" .}}
|
{{template "repo/issue/view_title" .}}
|
||||||
{{template "repo/pulls/tab_menu" .}}
|
{{template "repo/pulls/tab_menu" .}}
|
||||||
{{template "repo/diff/box" .}}
|
{{template "repo/diff/box" .}}
|
||||||
|
@ -56,7 +56,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
|
||||||
{{end}}
|
{{end}}
|
||||||
<span class="ahead"><a href="{{$.RepoLink}}/compare/{{.TagName | PathEscapeSegments}}{{if .Target}}...{{.Target | PathEscapeSegments}}{{end}}">{{$.locale.Tr "repo.release.ahead.commits" .NumCommitsBehind | Str2html}}</a> {{$.locale.Tr "repo.tag.ahead.target" $.DefaultBranch}}</span>
|
<span class="ahead"><a href="{{$.RepoLink}}/compare/{{.TagName | PathEscapeSegments}}...{{.TargetBehind | PathEscapeSegments}}">{{$.locale.Tr "repo.release.ahead.commits" .NumCommitsBehind | Str2html}}</a> {{$.locale.Tr "repo.tag.ahead.target" .TargetBehind}}</span>
|
||||||
</p>
|
</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p class="text grey">
|
<p class="text grey">
|
||||||
@ -77,7 +77,7 @@
|
|||||||
<span class="time">{{TimeSinceUnix .CreatedUnix $.locale}}</span>
|
<span class="time">{{TimeSinceUnix .CreatedUnix $.locale}}</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if not .IsDraft}}
|
{{if not .IsDraft}}
|
||||||
| <span class="ahead"><a href="{{$.RepoLink}}/compare/{{.TagName | PathEscapeSegments}}...{{.Target | PathEscapeSegments}}">{{$.locale.Tr "repo.release.ahead.commits" .NumCommitsBehind | Str2html}}</a> {{$.locale.Tr "repo.release.ahead.target" .Target}}</span>
|
| <span class="ahead"><a href="{{$.RepoLink}}/compare/{{.TagName | PathEscapeSegments}}...{{.TargetBehind | PathEscapeSegments}}">{{$.locale.Tr "repo.release.ahead.commits" .NumCommitsBehind | Str2html}}</a> {{$.locale.Tr "repo.release.ahead.target" .TargetBehind}}</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
</p>
|
</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
26
templates/swagger/v1_json.tmpl
generated
26
templates/swagger/v1_json.tmpl
generated
@ -4498,6 +4498,24 @@
|
|||||||
"name": "sha",
|
"name": "sha",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "include diff stats for every commit (disable for speedup, default 'true')",
|
||||||
|
"name": "stat",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "include verification for every commit (disable for speedup, default 'true')",
|
||||||
|
"name": "verification",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "include a list of affected files for every commit (disable for speedup, default 'true')",
|
||||||
|
"name": "files",
|
||||||
|
"in": "query"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@ -16145,10 +16163,16 @@
|
|||||||
"x-go-name": "BranchName"
|
"x-go-name": "BranchName"
|
||||||
},
|
},
|
||||||
"old_branch_name": {
|
"old_branch_name": {
|
||||||
"description": "Name of the old branch to create from",
|
"description": "Deprecated: true\nName of the old branch to create from",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"uniqueItems": true,
|
"uniqueItems": true,
|
||||||
"x-go-name": "OldBranchName"
|
"x-go-name": "OldBranchName"
|
||||||
|
},
|
||||||
|
"old_ref_name": {
|
||||||
|
"description": "Name of the old branch/tag/commit to create from",
|
||||||
|
"type": "string",
|
||||||
|
"uniqueItems": true,
|
||||||
|
"x-go-name": "OldRefName"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<!-- TODO: make template org and user can share -->
|
<!-- TODO: make template org and user can share -->
|
||||||
{{with .ContextUser}}
|
{{if or (.IsPackagesPage) (.PageIsViewProjects)}}
|
||||||
<div class="ui container">
|
{{with .ContextUser}}
|
||||||
|
<div class="ui container">
|
||||||
<div class="ui vertically grid head">
|
<div class="ui vertically grid head">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="ui header">
|
<div class="ui header">
|
||||||
@ -14,11 +15,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<div class="ui tabs container">
|
<div class="ui tabs container">
|
||||||
<div class="ui secondary stackable pointing menu">
|
<div class="ui secondary stackable pointing menu">
|
||||||
<a class="item" href="{{.ContextUser.HomeLink}}">
|
{{if .ProfileReadme}}
|
||||||
|
<a class='{{if or (eq .TabName "overview") (and (eq .TabName "") (not .IsPackagesPage) (not .PageIsViewProjects))}}active {{end}}item' href="{{.ContextUser.HomeLink}}?tab=overview">
|
||||||
|
{{svg "octicon-info"}} {{.locale.Tr "user.overview"}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
<a class="{{if or (eq .TabName "repositories") (and (eq .TabName "") (not .IsPackagesPage) (not .PageIsViewProjects) (not .ProfileReadme))}}active {{end}} item" href="{{.ContextUser.HomeLink}}?tab=repositories">
|
||||||
{{svg "octicon-repo"}} {{.locale.Tr "user.repositories"}}
|
{{svg "octicon-repo"}} {{.locale.Tr "user.repositories"}}
|
||||||
{{if .ContextUser.NumRepos}}
|
{{if .ContextUser.NumRepos}}
|
||||||
<div class="ui small label">{{.ContextUser.NumRepos}}</div>
|
<div class="ui small label">{{.ContextUser.NumRepos}}</div>
|
||||||
|
@ -121,40 +121,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="ui eleven wide column">
|
<div class="ui eleven wide column">
|
||||||
<div class="ui secondary stackable pointing tight menu">
|
<div class="ui secondary stackable pointing tight menu">
|
||||||
<a class='{{if and (ne .TabName "activity") (ne .TabName "following") (ne .TabName "followers") (ne .TabName "stars") (ne .TabName "watching") (ne .TabName "projects") (ne .TabName "code")}}active {{end}}item' href="{{.ContextUser.HomeLink}}">
|
{{template "user/overview/header" .}}
|
||||||
{{svg "octicon-repo"}} {{.locale.Tr "user.repositories"}}
|
|
||||||
{{if .ContextUser.NumRepos}}
|
|
||||||
<div class="ui small label">{{.ContextUser.NumRepos}}</div>
|
|
||||||
{{end}}
|
|
||||||
</a>
|
|
||||||
<a href="{{.ContextUser.HomeLink}}/-/projects" class="{{if eq .TabName "projects"}}active {{end}}item">
|
|
||||||
{{svg "octicon-project-symlink"}} {{.locale.Tr "user.projects"}}
|
|
||||||
</a>
|
|
||||||
{{if .IsPackageEnabled}}
|
|
||||||
<a class='{{if eq .TabName "packages"}}active {{end}}item' href="{{.ContextUser.HomeLink}}/-/packages">
|
|
||||||
{{svg "octicon-package"}} {{.locale.Tr "packages.title"}}
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
{{if and (not $.UnitTypeCode.UnitGlobalDisabled) .IsRepoIndexerEnabled}}
|
|
||||||
<a class='{{if eq .TabName "code"}}active {{end}}item' href="{{.ContextUser.HomeLink}}/-/code">
|
|
||||||
{{svg "octicon-code"}} {{.locale.Tr "user.code"}}
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
<a class='{{if eq .TabName "activity"}}active {{end}}item' href="{{.ContextUser.HomeLink}}?tab=activity">
|
|
||||||
{{svg "octicon-rss"}} {{.locale.Tr "user.activity"}}
|
|
||||||
</a>
|
|
||||||
{{if not .DisableStars}}
|
|
||||||
<a class='{{if eq .TabName "stars"}}active {{end}}item' href="{{.ContextUser.HomeLink}}?tab=stars">
|
|
||||||
{{svg "octicon-star"}} {{.locale.Tr "user.starred"}}
|
|
||||||
{{if .ContextUser.NumStars}}
|
|
||||||
<div class="ui small label">{{.ContextUser.NumStars}}</div>
|
|
||||||
{{end}}
|
|
||||||
</a>
|
|
||||||
{{else}}
|
|
||||||
<a class='{{if eq .TabName "watching"}}active {{end}}item' href="{{.ContextUser.HomeLink}}?tab=watching">
|
|
||||||
{{svg "octicon-eye"}} {{.locale.Tr "user.watched"}}
|
|
||||||
</a>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if eq .TabName "activity"}}
|
{{if eq .TabName "activity"}}
|
||||||
@ -177,10 +144,12 @@
|
|||||||
{{template "repo/user_cards" .}}
|
{{template "repo/user_cards" .}}
|
||||||
{{else if eq .TabName "followers"}}
|
{{else if eq .TabName "followers"}}
|
||||||
{{template "repo/user_cards" .}}
|
{{template "repo/user_cards" .}}
|
||||||
{{else}}
|
{{else if or (eq .TabName "repositories") (not .ProfileReadme)}}
|
||||||
{{template "explore/repo_search" .}}
|
{{template "explore/repo_search" .}}
|
||||||
{{template "explore/repo_list" .}}
|
{{template "explore/repo_list" .}}
|
||||||
{{template "base/paginate" .}}
|
{{template "base/paginate" .}}
|
||||||
|
{{else if .ProfileReadme}}
|
||||||
|
<div id="readme_profile" class="render-content markup"> {{$.ProfileReadme|Str2html}} </div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -143,10 +143,10 @@ func TestViewReleaseListNoLogin(t *testing.T) {
|
|||||||
|
|
||||||
htmlDoc := NewHTMLParser(t, rsp.Body)
|
htmlDoc := NewHTMLParser(t, rsp.Body)
|
||||||
releases := htmlDoc.Find("#release-list li.ui.grid")
|
releases := htmlDoc.Find("#release-list li.ui.grid")
|
||||||
assert.Equal(t, 3, releases.Length())
|
assert.Equal(t, 5, releases.Length())
|
||||||
|
|
||||||
links := make([]string, 0, 3)
|
links := make([]string, 0, 5)
|
||||||
commitsToMain := make([]string, 0, 3)
|
commitsToMain := make([]string, 0, 5)
|
||||||
releases.Each(func(i int, s *goquery.Selection) {
|
releases.Each(func(i int, s *goquery.Selection) {
|
||||||
link, exist := s.Find(".release-list-title a").Attr("href")
|
link, exist := s.Find(".release-list-title a").Attr("href")
|
||||||
if !exist {
|
if !exist {
|
||||||
@ -158,11 +158,15 @@ func TestViewReleaseListNoLogin(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
assert.EqualValues(t, []string{
|
assert.EqualValues(t, []string{
|
||||||
|
"/user2/repo-release/releases/tag/empty-target-branch",
|
||||||
|
"/user2/repo-release/releases/tag/non-existing-target-branch",
|
||||||
"/user2/repo-release/releases/tag/v2.0",
|
"/user2/repo-release/releases/tag/v2.0",
|
||||||
"/user2/repo-release/releases/tag/v1.1",
|
"/user2/repo-release/releases/tag/v1.1",
|
||||||
"/user2/repo-release/releases/tag/v1.0",
|
"/user2/repo-release/releases/tag/v1.0",
|
||||||
}, links)
|
}, links)
|
||||||
assert.EqualValues(t, []string{
|
assert.EqualValues(t, []string{
|
||||||
|
"1 commits", // like v1.1
|
||||||
|
"1 commits", // like v1.1
|
||||||
"0 commits",
|
"0 commits",
|
||||||
"1 commits", // should be 3 commits ahead and 2 commits behind, but not implemented yet
|
"1 commits", // should be 3 commits ahead and 2 commits behind, but not implemented yet
|
||||||
"3 commits",
|
"3 commits",
|
||||||
|
@ -6,6 +6,10 @@
|
|||||||
margin: 0 0.5em;
|
margin: 0 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.board.sortable .board-card {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
.board-column {
|
.board-column {
|
||||||
background-color: var(--color-project-board-bg) !important;
|
background-color: var(--color-project-board-bg) !important;
|
||||||
border: 1px solid var(--color-secondary) !important;
|
border: 1px solid var(--color-secondary) !important;
|
||||||
@ -80,7 +84,6 @@
|
|||||||
.board-card {
|
.board-card {
|
||||||
margin: 4px 2px !important;
|
margin: 4px 2px !important;
|
||||||
border-radius: 5px !important;
|
border-radius: 5px !important;
|
||||||
cursor: move;
|
|
||||||
width: calc(100% - 4px) !important;
|
width: calc(100% - 4px) !important;
|
||||||
padding: 0.5rem !important;
|
padding: 0.5rem !important;
|
||||||
min-height: auto !important;
|
min-height: auto !important;
|
||||||
|
@ -140,6 +140,13 @@
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#readme_profile {
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 0.28571429rem;
|
||||||
|
background: var(--color-card);
|
||||||
|
border: 1px solid var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
#notification_table tr {
|
#notification_table tr {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,14 @@
|
|||||||
Please also update the template file above if this vue is modified.
|
Please also update the template file above if this vue is modified.
|
||||||
-->
|
-->
|
||||||
<template>
|
<template>
|
||||||
<SvgIcon name="octicon-check-circle-fill" class="text green" :size="size" :class-name="className" v-if="status === 'success'"/>
|
<span :data-tooltip-content="localeStatus">
|
||||||
<SvgIcon name="octicon-skip" class="text grey" :size="size" :class-name="className" v-else-if="status === 'skipped'"/>
|
<SvgIcon name="octicon-check-circle-fill" class="text green" :size="size" :class-name="className" v-if="status === 'success'"/>
|
||||||
<SvgIcon name="octicon-clock" class="text yellow" :size="size" :class-name="className" v-else-if="status === 'waiting'"/>
|
<SvgIcon name="octicon-skip" class="text grey" :size="size" :class-name="className" v-else-if="status === 'skipped'"/>
|
||||||
<SvgIcon name="octicon-blocked" class="text yellow" :size="size" :class-name="className" v-else-if="status === 'blocked'"/>
|
<SvgIcon name="octicon-clock" class="text yellow" :size="size" :class-name="className" v-else-if="status === 'waiting'"/>
|
||||||
<SvgIcon name="octicon-meter" class="text yellow" :size="size" :class-name="'job-status-rotate ' + className" v-else-if="status === 'running'"/>
|
<SvgIcon name="octicon-blocked" class="text yellow" :size="size" :class-name="className" v-else-if="status === 'blocked'"/>
|
||||||
<SvgIcon name="octicon-x-circle-fill" class="text red" :size="size" v-else/>
|
<SvgIcon name="octicon-meter" class="text yellow" :size="size" :class-name="'job-status-rotate ' + className" v-else-if="status === 'running'"/>
|
||||||
|
<SvgIcon name="octicon-x-circle-fill" class="text red" :size="size" v-else/>
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -27,6 +29,10 @@ export default {
|
|||||||
className: {
|
className: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
|
},
|
||||||
|
localeStatus: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div class="action-view-container">
|
<div class="action-view-container">
|
||||||
<div class="action-view-header">
|
<div class="action-view-header">
|
||||||
<div class="action-info-summary gt-ac">
|
<div class="action-info-summary gt-ac">
|
||||||
<ActionRunStatus :status="run.status" :size="20"/>
|
<ActionRunStatus :locale-status="locale.status[run.status]" :status="run.status" :size="20"/>
|
||||||
<div class="action-title">
|
<div class="action-title">
|
||||||
{{ run.title }}
|
{{ run.title }}
|
||||||
</div>
|
</div>
|
||||||
@ -32,7 +32,7 @@
|
|||||||
<div class="job-brief-list">
|
<div class="job-brief-list">
|
||||||
<div class="job-brief-item" v-for="(job, index) in run.jobs" :key="job.id">
|
<div class="job-brief-item" v-for="(job, index) in run.jobs" :key="job.id">
|
||||||
<a class="job-brief-link" :href="run.link+'/jobs/'+index">
|
<a class="job-brief-link" :href="run.link+'/jobs/'+index">
|
||||||
<ActionRunStatus :status="job.status"/>
|
<ActionRunStatus :locale-status="locale.status[job.status]" :status="job.status"/>
|
||||||
<span class="ui text gt-mx-3">{{ job.name }}</span>
|
<span class="ui text gt-mx-3">{{ job.name }}</span>
|
||||||
</a>
|
</a>
|
||||||
<span class="step-summary-duration">{{ job.duration }}</span>
|
<span class="step-summary-duration">{{ job.duration }}</span>
|
||||||
@ -319,6 +319,16 @@ export function initRepositoryActionView() {
|
|||||||
approve: el.getAttribute('data-locale-approve'),
|
approve: el.getAttribute('data-locale-approve'),
|
||||||
cancel: el.getAttribute('data-locale-cancel'),
|
cancel: el.getAttribute('data-locale-cancel'),
|
||||||
rerun: el.getAttribute('data-locale-rerun'),
|
rerun: el.getAttribute('data-locale-rerun'),
|
||||||
|
status: {
|
||||||
|
unknown: el.getAttribute('data-locale-status-unknown'),
|
||||||
|
waiting: el.getAttribute('data-locale-status-waiting'),
|
||||||
|
running: el.getAttribute('data-locale-status-running'),
|
||||||
|
success: el.getAttribute('data-locale-status-success'),
|
||||||
|
failure: el.getAttribute('data-locale-status-failure'),
|
||||||
|
cancelled: el.getAttribute('data-locale-status-cancelled'),
|
||||||
|
skipped: el.getAttribute('data-locale-status-skipped'),
|
||||||
|
blocked: el.getAttribute('data-locale-status-blocked'),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
view.mount(el);
|
view.mount(el);
|
||||||
|
@ -36,7 +36,7 @@ function moveIssue({item, from, to, oldIndex}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function initRepoProjectSortable() {
|
async function initRepoProjectSortable() {
|
||||||
const els = document.querySelectorAll('#project-board > .board');
|
const els = document.querySelectorAll('#project-board > .board.sortable');
|
||||||
if (!els.length) return;
|
if (!els.length) return;
|
||||||
|
|
||||||
const {Sortable} = await import(/* webpackChunkName: "sortable" */'sortablejs');
|
const {Sortable} = await import(/* webpackChunkName: "sortable" */'sortablejs');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user