Compare commits

...

9 Commits

Author SHA1 Message Date
oliverpool
8030614386
fix: release page for empty or non-existing target (#24470)
Fixes #24145

To solve the bug, I added a "computed" `TargetBehind` field to the
`Release` model, which indicates the target branch of a release.
This is particularly useful if the target branch was deleted in the
meantime (or is empty).

I also did a micro-optimization in `calReleaseNumCommitsBehind`. Instead
of checking that a branch exists and then call `GetBranchCommit`, I
immediately call `GetBranchCommit` and handle the `git.ErrNotExist`
error.

This optimization is covered by the added unit test.
2023-05-10 11:43:55 +08:00
Matthew Walowski
5930ab5fdf
Filter get single commit (#24613)
Pretty much the same thing as #24568 but for getting a single commit
instead of getting a list of commits
2023-05-10 09:34:07 +08:00
Yarden Shoham
9a0652f0b2
Attach a tooltip to the action status icon (#24614)
To clearly communicate the current state of the action

![image](https://github.com/go-gitea/gitea/assets/20454870/5d6de6b9-f34f-417d-b08e-fcd1b99b3079)
![image](https://github.com/go-gitea/gitea/assets/20454870/b976676a-4525-43e7-866f-8933be1a5dfd)
![image](https://github.com/go-gitea/gitea/assets/20454870/2e0a55fe-658f-4242-83de-b857a8b55f31)
![image](https://github.com/go-gitea/gitea/assets/20454870/6b42bcd1-c499-41ac-8419-1c4e60085d47)
![image](https://github.com/go-gitea/gitea/assets/20454870/363fcff8-fe61-4363-a04b-2db93cfc4fa3)
![image](https://github.com/go-gitea/gitea/assets/20454870/f8f59b68-93de-4f31-b9b0-24d94990d1d0)

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
2023-05-09 21:39:16 +02:00
Yarden Shoham
4f1065030f
Use official Vue extension in Gitpod (#24609)
The previous one didn't work well

---------

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
Co-authored-by: silverwind <me@silverwind.io>
2023-05-09 18:03:50 +00:00
来自村里的小螃蟹
cd9a13ebb4
Create a branch directly from commit on the create branch API (#22956)
#### Added
- API: Create a branch directly from commit on the create branch API
- Added `old_ref_name` parameter to allow creating a new branch from a
specific commit, tag, or branch.
- Deprecated `old_branch_name` parameter in favor of the new
`old_ref_name` parameter.

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-05-09 18:22:32 +08:00
wxiaoguang
023a048f52
Make repository response support HTTP range request (#24592)
Replace #20480
Replace #18448

Close #16414
2023-05-09 15:34:36 +08:00
Nicholas Pease
c090f87a8d
Add Gitea Profile Readmes (#23260)
Implements displaying a README.md file present in a users ```.profile```
repository on the users profile page. If no such repository/file is
present, the user's profile page remains unchanged.

Example of user with ```.profile/README.md```

![image](https://user-images.githubusercontent.com/34464552/222757202-5d53ac62-60d9-432f-b9e3-2537ffa91041.png)

Example of user without ```.profile/README.md```

![image](https://user-images.githubusercontent.com/34464552/222759972-576e058b-acd4-47ac-be33-38a7cb58cc81.png)

This pull request closes the feature request in #12233 

Special thanks to @techknowlogick for the help in the Gitea discord!

---------

Co-authored-by: techknowlogick <techknowlogick@gitea.io>
Co-authored-by: Yarden Shoham <hrsi88@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: yp05327 <576951401@qq.com>
Co-authored-by: Yarden Shoham <git@yardenshoham.com>
2023-05-09 05:57:24 +00:00
wxiaoguang
b6fc2cdf82
Make diff view full width again (#24598)
Regression of #24459 , [the related
line](https://github.com/go-gitea/gitea/pull/24459/files#diff-f255004de8d715ff40852710390429bf2a06e7e33a4e3f8ad568af636557ac71L8)

The PR file diff view needs to be full-screen width.
2023-05-09 05:21:03 +00:00
yp05327
2ee72d011f
Add permission check for moving issue action in project view page (#24589)
Fix #22954
Only users who have write permission can move issues in the project view page.
2023-05-09 00:50:16 -04:00
42 changed files with 784 additions and 317 deletions

View File

@ -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

View 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.

View 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

View File

@ -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

View File

@ -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)
} }

View File

@ -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
View 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
View 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)
}

View 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:])
})
}

View File

@ -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)
} }

View File

@ -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

View File

@ -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

View File

@ -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
} }

View File

@ -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

View File

@ -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) {

View File

@ -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
View 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)
}

View File

@ -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

View File

@ -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)

View File

@ -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
} }

View File

@ -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)
}
}

View File

@ -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
}
}
} }

View File

@ -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{

View File

@ -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

View File

@ -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}}">

View File

@ -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">

View File

@ -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>

View File

@ -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>

View File

@ -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" .}}

View File

@ -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" .}}

View File

@ -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" .}}

View File

@ -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}}

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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",

View File

@ -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;

View File

@ -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;
} }

View File

@ -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: ''
} }
}, },
}; };

View File

@ -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);

View File

@ -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');