mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-20 01:32:41 +02:00
Merge remote-tracking branch 'upstream/main' into ssh-mirroring
This commit is contained in:
@@ -20,7 +20,7 @@ func IsArtifactV4(art *actions_model.ActionArtifact) bool {
|
||||
|
||||
func DownloadArtifactV4ServeDirectOnly(ctx *context.Base, art *actions_model.ActionArtifact) (bool, error) {
|
||||
if setting.Actions.ArtifactStorage.ServeDirect() {
|
||||
u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath, nil)
|
||||
u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath, ctx.Req.Method, nil)
|
||||
if u != nil && err == nil {
|
||||
ctx.Redirect(u.String(), http.StatusFound)
|
||||
return true, nil
|
||||
|
||||
@@ -20,6 +20,7 @@ const (
|
||||
GitlabLanguage = "gitlab-language"
|
||||
Lockable = "lockable"
|
||||
Filter = "filter"
|
||||
Diff = "diff"
|
||||
)
|
||||
|
||||
var LinguistAttributes = []string{
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
package git
|
||||
|
||||
import "path"
|
||||
|
||||
// CommitInfo describes the first commit with the provided entry
|
||||
type CommitInfo struct {
|
||||
Entry *TreeEntry
|
||||
@@ -12,11 +10,14 @@ type CommitInfo struct {
|
||||
SubmoduleFile *CommitSubmoduleFile
|
||||
}
|
||||
|
||||
func getCommitInfoSubmoduleFile(repoLink string, entry *TreeEntry, commit *Commit, treePathDir string) (*CommitSubmoduleFile, error) {
|
||||
fullPath := path.Join(treePathDir, entry.Name())
|
||||
func GetCommitInfoSubmoduleFile(repoLink, fullPath string, commit *Commit, refCommitID ObjectID) (*CommitSubmoduleFile, error) {
|
||||
submodule, err := commit.GetSubModule(fullPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewCommitSubmoduleFile(repoLink, fullPath, submodule.URL, entry.ID.String()), nil
|
||||
if submodule == nil {
|
||||
// unable to find submodule from ".gitmodules" file
|
||||
return NewCommitSubmoduleFile(repoLink, fullPath, "", refCommitID.String()), nil
|
||||
}
|
||||
return NewCommitSubmoduleFile(repoLink, fullPath, submodule.URL, refCommitID.String()), nil
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *
|
||||
|
||||
// If the entry is a submodule, add a submodule file for this
|
||||
if entry.IsSubModule() {
|
||||
commitsInfo[i].SubmoduleFile, err = getCommitInfoSubmoduleFile(repoLink, entry, commit, treePath)
|
||||
commitsInfo[i].SubmoduleFile, err = GetCommitInfoSubmoduleFile(repoLink, path.Join(treePath, entry.Name()), commit, entry.ID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *
|
||||
|
||||
// If the entry is a submodule, add a submodule file for this
|
||||
if entry.IsSubModule() {
|
||||
commitsInfo[i].SubmoduleFile, err = getCommitInfoSubmoduleFile(repoLink, entry, commit, treePath)
|
||||
commitsInfo[i].SubmoduleFile, err = GetCommitInfoSubmoduleFile(repoLink, path.Join(treePath, entry.Name()), commit, entry.ID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -120,6 +121,23 @@ func TestEntries_GetCommitsInfo(t *testing.T) {
|
||||
defer clonedRepo1.Close()
|
||||
|
||||
testGetCommitsInfo(t, clonedRepo1)
|
||||
|
||||
t.Run("NonExistingSubmoduleAsNil", func(t *testing.T) {
|
||||
commit, err := bareRepo1.GetCommit("HEAD")
|
||||
require.NoError(t, err)
|
||||
treeEntry, err := commit.GetTreeEntryByPath("file1.txt")
|
||||
require.NoError(t, err)
|
||||
cisf, err := GetCommitInfoSubmoduleFile("/any/repo-link", "file1.txt", commit, treeEntry.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, &CommitSubmoduleFile{
|
||||
repoLink: "/any/repo-link",
|
||||
fullPath: "file1.txt",
|
||||
refURL: "",
|
||||
refID: "e2129701f1a4d54dc44f03c93bca0a2aec7c5449",
|
||||
}, cisf)
|
||||
// since there is no refURL, it means that the submodule info doesn't exist, so it won't have a web link
|
||||
assert.Nil(t, cisf.SubmoduleWebLinkTree(t.Context()))
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkEntries_GetCommitsInfo(b *testing.B) {
|
||||
|
||||
@@ -35,7 +35,8 @@ func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) {
|
||||
return c.submoduleCache, nil
|
||||
}
|
||||
|
||||
// GetSubModule get the submodule according entry name
|
||||
// GetSubModule gets the submodule by the entry name.
|
||||
// It returns "nil, nil" if the submodule does not exist, caller should always remember to check the "nil"
|
||||
func (c *Commit) GetSubModule(entryName string) (*SubModule, error) {
|
||||
modules, err := c.GetSubModules()
|
||||
if err != nil {
|
||||
|
||||
@@ -29,16 +29,20 @@ func NewCommitSubmoduleFile(repoLink, fullPath, refURL, refID string) *CommitSub
|
||||
return &CommitSubmoduleFile{repoLink: repoLink, fullPath: fullPath, refURL: refURL, refID: refID}
|
||||
}
|
||||
|
||||
// RefID returns the commit ID of the submodule, it returns empty string for nil receiver
|
||||
func (sf *CommitSubmoduleFile) RefID() string {
|
||||
if sf == nil {
|
||||
return ""
|
||||
}
|
||||
return sf.refID
|
||||
}
|
||||
|
||||
func (sf *CommitSubmoduleFile) getWebLinkInTargetRepo(ctx context.Context, moreLinkPath string) *SubmoduleWebLink {
|
||||
if sf == nil {
|
||||
if sf == nil || sf.refURL == "" {
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(sf.refURL, "../") {
|
||||
targetLink := path.Join(sf.repoLink, path.Dir(sf.fullPath), sf.refURL)
|
||||
targetLink := path.Join(sf.repoLink, sf.refURL)
|
||||
return &SubmoduleWebLink{RepoWebLink: targetLink, CommitWebLink: targetLink + moreLinkPath}
|
||||
}
|
||||
if !sf.parsed {
|
||||
@@ -53,14 +57,13 @@ func (sf *CommitSubmoduleFile) getWebLinkInTargetRepo(ctx context.Context, moreL
|
||||
}
|
||||
|
||||
// SubmoduleWebLinkTree tries to make the submodule's tree link in its own repo, it also works on "nil" receiver
|
||||
// It returns nil if the submodule does not have a valid URL or is nil
|
||||
func (sf *CommitSubmoduleFile) SubmoduleWebLinkTree(ctx context.Context, optCommitID ...string) *SubmoduleWebLink {
|
||||
if sf == nil {
|
||||
return nil
|
||||
}
|
||||
return sf.getWebLinkInTargetRepo(ctx, "/tree/"+util.OptionalArg(optCommitID, sf.refID))
|
||||
return sf.getWebLinkInTargetRepo(ctx, "/tree/"+util.OptionalArg(optCommitID, sf.RefID()))
|
||||
}
|
||||
|
||||
// SubmoduleWebLinkCompare tries to make the submodule's compare link in its own repo, it also works on "nil" receiver
|
||||
// It returns nil if the submodule does not have a valid URL or is nil
|
||||
func (sf *CommitSubmoduleFile) SubmoduleWebLinkCompare(ctx context.Context, commitID1, commitID2 string) *SubmoduleWebLink {
|
||||
return sf.getWebLinkInTargetRepo(ctx, "/compare/"+commitID1+"..."+commitID2)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
func TestCommitSubmoduleLink(t *testing.T) {
|
||||
assert.Nil(t, (*CommitSubmoduleFile)(nil).SubmoduleWebLinkTree(t.Context()))
|
||||
assert.Nil(t, (*CommitSubmoduleFile)(nil).SubmoduleWebLinkCompare(t.Context(), "", ""))
|
||||
assert.Nil(t, (&CommitSubmoduleFile{}).SubmoduleWebLinkTree(t.Context()))
|
||||
assert.Nil(t, (&CommitSubmoduleFile{}).SubmoduleWebLinkCompare(t.Context(), "", ""))
|
||||
|
||||
t.Run("GitHubRepo", func(t *testing.T) {
|
||||
sf := NewCommitSubmoduleFile("/any/repo-link", "full-path", "git@github.com:user/repo.git", "aaaa")
|
||||
@@ -30,7 +32,7 @@ func TestCommitSubmoduleLink(t *testing.T) {
|
||||
assert.Equal(t, "/subpath/user/repo", wl.RepoWebLink)
|
||||
assert.Equal(t, "/subpath/user/repo/tree/aaaa", wl.CommitWebLink)
|
||||
|
||||
sf = NewCommitSubmoduleFile("/subpath/any/repo-home-link", "dir/submodule", "../../../user/repo", "aaaa")
|
||||
sf = NewCommitSubmoduleFile("/subpath/any/repo-home-link", "dir/submodule", "../../user/repo", "aaaa")
|
||||
wl = sf.SubmoduleWebLinkCompare(t.Context(), "1111", "2222")
|
||||
assert.Equal(t, "/subpath/user/repo", wl.RepoWebLink)
|
||||
assert.Equal(t, "/subpath/user/repo/compare/1111...2222", wl.CommitWebLink)
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// ObjectCache provides thread-safe cache operations.
|
||||
@@ -106,3 +108,16 @@ func HashFilePathForWebUI(s string) string {
|
||||
_, _ = h.Write([]byte(s))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func SplitCommitTitleBody(commitMessage string, titleRuneLimit int) (title, body string) {
|
||||
title, body, _ = strings.Cut(commitMessage, "\n")
|
||||
title, title2 := util.EllipsisTruncateRunes(title, titleRuneLimit)
|
||||
if title2 != "" {
|
||||
if body == "" {
|
||||
body = title2
|
||||
} else {
|
||||
body = title2 + "\n" + body
|
||||
}
|
||||
}
|
||||
return title, body
|
||||
}
|
||||
|
||||
@@ -15,3 +15,17 @@ func TestHashFilePathForWebUI(t *testing.T) {
|
||||
HashFilePathForWebUI("foobar"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestSplitCommitTitleBody(t *testing.T) {
|
||||
title, body := SplitCommitTitleBody("啊bcdefg", 4)
|
||||
assert.Equal(t, "啊…", title)
|
||||
assert.Equal(t, "…bcdefg", body)
|
||||
|
||||
title, body = SplitCommitTitleBody("abcdefg\n1234567", 4)
|
||||
assert.Equal(t, "a…", title)
|
||||
assert.Equal(t, "…bcdefg\n1234567", body)
|
||||
|
||||
title, body = SplitCommitTitleBody("abcdefg\n1234567", 100)
|
||||
assert.Equal(t, "abcdefg", title)
|
||||
assert.Equal(t, "1234567", body)
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@ func (p *PullRequest) IsForkPullRequest() bool {
|
||||
return p.Head.RepoFullName() != p.Base.RepoFullName()
|
||||
}
|
||||
|
||||
// GetGitRefName returns pull request relative path to head
|
||||
func (p PullRequest) GetGitRefName() string {
|
||||
// GetGitHeadRefName returns pull request relative path to head
|
||||
func (p PullRequest) GetGitHeadRefName() string {
|
||||
return fmt.Sprintf("%s%d/head", git.PullPrefix, p.Number)
|
||||
}
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ func (s *ContentStore) ShouldServeDirect() bool {
|
||||
return setting.Packages.Storage.ServeDirect()
|
||||
}
|
||||
|
||||
func (s *ContentStore) GetServeDirectURL(key BlobHash256Key, filename string, reqParams url.Values) (*url.URL, error) {
|
||||
return s.store.URL(KeyToRelativePath(key), filename, reqParams)
|
||||
func (s *ContentStore) GetServeDirectURL(key BlobHash256Key, filename, method string, reqParams url.Values) (*url.URL, error) {
|
||||
return s.store.URL(KeyToRelativePath(key), filename, method, reqParams)
|
||||
}
|
||||
|
||||
// FIXME: Workaround to be removed in v1.20
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package session
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"net/http"
|
||||
|
||||
"gitea.com/go-chi/session"
|
||||
)
|
||||
|
||||
type mockMemRawStore struct {
|
||||
s *session.MemStore
|
||||
}
|
||||
|
||||
var _ session.RawStore = (*mockMemRawStore)(nil)
|
||||
|
||||
func (m *mockMemRawStore) Set(k, v any) error {
|
||||
// We need to use gob to encode the value, to make it have the same behavior as other stores and catch abuses.
|
||||
// Because gob needs to "Register" the type before it can encode it, and it's unable to decode a struct to "any" so use a map to help to decode the value.
|
||||
var buf bytes.Buffer
|
||||
if err := gob.NewEncoder(&buf).Encode(map[string]any{"v": v}); err != nil {
|
||||
return err
|
||||
}
|
||||
return m.s.Set(k, buf.Bytes())
|
||||
}
|
||||
|
||||
func (m *mockMemRawStore) Get(k any) (ret any) {
|
||||
v, ok := m.s.Get(k).([]byte)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var w map[string]any
|
||||
_ = gob.NewDecoder(bytes.NewBuffer(v)).Decode(&w)
|
||||
return w["v"]
|
||||
}
|
||||
|
||||
func (m *mockMemRawStore) Delete(k any) error {
|
||||
return m.s.Delete(k)
|
||||
}
|
||||
|
||||
func (m *mockMemRawStore) ID() string {
|
||||
return m.s.ID()
|
||||
}
|
||||
|
||||
func (m *mockMemRawStore) Release() error {
|
||||
return m.s.Release()
|
||||
}
|
||||
|
||||
func (m *mockMemRawStore) Flush() error {
|
||||
return m.s.Flush()
|
||||
}
|
||||
|
||||
type mockMemStore struct {
|
||||
*mockMemRawStore
|
||||
}
|
||||
|
||||
var _ Store = (*mockMemStore)(nil)
|
||||
|
||||
func (m mockMemStore) Destroy(writer http.ResponseWriter, request *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewMockMemStore(sid string) Store {
|
||||
return &mockMemStore{&mockMemRawStore{session.NewMemStore(sid)}}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package session
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"gitea.com/go-chi/session"
|
||||
)
|
||||
|
||||
type MockStore struct {
|
||||
*session.MemStore
|
||||
}
|
||||
|
||||
func (m *MockStore) Destroy(writer http.ResponseWriter, request *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockStoreContextKeyStruct struct{}
|
||||
|
||||
var MockStoreContextKey = mockStoreContextKeyStruct{}
|
||||
|
||||
func NewMockStore(sid string) *MockStore {
|
||||
return &MockStore{session.NewMemStore(sid)}
|
||||
}
|
||||
+11
-11
@@ -11,25 +11,25 @@ import (
|
||||
"gitea.com/go-chi/session"
|
||||
)
|
||||
|
||||
// Store represents a session store
|
||||
type RawStore = session.RawStore
|
||||
|
||||
type Store interface {
|
||||
Get(any) any
|
||||
Set(any, any) error
|
||||
Delete(any) error
|
||||
ID() string
|
||||
Release() error
|
||||
Flush() error
|
||||
RawStore
|
||||
Destroy(http.ResponseWriter, *http.Request) error
|
||||
}
|
||||
|
||||
type mockStoreContextKeyStruct struct{}
|
||||
|
||||
var MockStoreContextKey = mockStoreContextKeyStruct{}
|
||||
|
||||
// RegenerateSession regenerates the underlying session and returns the new store
|
||||
func RegenerateSession(resp http.ResponseWriter, req *http.Request) (Store, error) {
|
||||
for _, f := range BeforeRegenerateSession {
|
||||
f(resp, req)
|
||||
}
|
||||
if setting.IsInTesting {
|
||||
if store, ok := req.Context().Value(MockStoreContextKey).(*MockStore); ok {
|
||||
return store, nil
|
||||
if store := req.Context().Value(MockStoreContextKey); store != nil {
|
||||
return store.(Store), nil
|
||||
}
|
||||
}
|
||||
return session.RegenerateSession(resp, req)
|
||||
@@ -37,8 +37,8 @@ func RegenerateSession(resp http.ResponseWriter, req *http.Request) (Store, erro
|
||||
|
||||
func GetContextSession(req *http.Request) Store {
|
||||
if setting.IsInTesting {
|
||||
if store, ok := req.Context().Value(MockStoreContextKey).(*MockStore); ok {
|
||||
return store
|
||||
if store := req.Context().Value(MockStoreContextKey); store != nil {
|
||||
return store.(Store)
|
||||
}
|
||||
}
|
||||
return session.GetSession(req)
|
||||
|
||||
@@ -22,8 +22,8 @@ type VirtualSessionProvider struct {
|
||||
provider session.Provider
|
||||
}
|
||||
|
||||
// Init initializes the cookie session provider with given root path.
|
||||
func (o *VirtualSessionProvider) Init(gclifetime int64, config string) error {
|
||||
// Init initializes the cookie session provider with the given config.
|
||||
func (o *VirtualSessionProvider) Init(gcLifetime int64, config string) error {
|
||||
var opts session.Options
|
||||
if err := json.Unmarshal([]byte(config), &opts); err != nil {
|
||||
return err
|
||||
@@ -52,7 +52,7 @@ func (o *VirtualSessionProvider) Init(gclifetime int64, config string) error {
|
||||
default:
|
||||
return fmt.Errorf("VirtualSessionProvider: Unknown Provider: %s", opts.Provider)
|
||||
}
|
||||
return o.provider.Init(gclifetime, opts.ProviderConfig)
|
||||
return o.provider.Init(gcLifetime, opts.ProviderConfig)
|
||||
}
|
||||
|
||||
// Read returns raw session store by session ID.
|
||||
|
||||
@@ -247,7 +247,7 @@ func (a *AzureBlobStorage) Delete(path string) error {
|
||||
}
|
||||
|
||||
// URL gets the redirect URL to a file. The presigned link is valid for 5 minutes.
|
||||
func (a *AzureBlobStorage) URL(path, name string, reqParams url.Values) (*url.URL, error) {
|
||||
func (a *AzureBlobStorage) URL(path, name, _ string, reqParams url.Values) (*url.URL, error) {
|
||||
blobClient := a.getBlobClient(path)
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
@@ -30,7 +30,7 @@ func (s discardStorage) Delete(_ string) error {
|
||||
return fmt.Errorf("%s", s)
|
||||
}
|
||||
|
||||
func (s discardStorage) URL(_, _ string, _ url.Values) (*url.URL, error) {
|
||||
func (s discardStorage) URL(_, _, _ string, _ url.Values) (*url.URL, error) {
|
||||
return nil, fmt.Errorf("%s", s)
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ func Test_discardStorage(t *testing.T) {
|
||||
assert.Error(t, err, string(tt))
|
||||
}
|
||||
{
|
||||
got, err := tt.URL("path", "name", nil)
|
||||
got, err := tt.URL("path", "name", "GET", nil)
|
||||
assert.Nil(t, got)
|
||||
assert.Errorf(t, err, string(tt))
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ func (l *LocalStorage) Delete(path string) error {
|
||||
}
|
||||
|
||||
// URL gets the redirect URL to a file
|
||||
func (l *LocalStorage) URL(path, name string, reqParams url.Values) (*url.URL, error) {
|
||||
func (l *LocalStorage) URL(path, name, _ string, reqParams url.Values) (*url.URL, error) {
|
||||
return nil, ErrURLNotSupported
|
||||
}
|
||||
|
||||
|
||||
@@ -279,7 +279,7 @@ func (m *MinioStorage) Delete(path string) error {
|
||||
}
|
||||
|
||||
// URL gets the redirect URL to a file. The presigned link is valid for 5 minutes.
|
||||
func (m *MinioStorage) URL(path, name string, serveDirectReqParams url.Values) (*url.URL, error) {
|
||||
func (m *MinioStorage) URL(path, name, method string, serveDirectReqParams url.Values) (*url.URL, error) {
|
||||
// copy serveDirectReqParams
|
||||
reqParams, err := url.ParseQuery(serveDirectReqParams.Encode())
|
||||
if err != nil {
|
||||
@@ -287,7 +287,12 @@ func (m *MinioStorage) URL(path, name string, serveDirectReqParams url.Values) (
|
||||
}
|
||||
// TODO it may be good to embed images with 'inline' like ServeData does, but we don't want to have to read the file, do we?
|
||||
reqParams.Set("response-content-disposition", "attachment; filename=\""+quoteEscaper.Replace(name)+"\"")
|
||||
u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(path), 5*time.Minute, reqParams)
|
||||
expires := 5 * time.Minute
|
||||
if method == http.MethodHead {
|
||||
u, err := m.client.PresignedHeadObject(m.ctx, m.bucket, m.buildMinioPath(path), expires, reqParams)
|
||||
return u, convertMinioErr(err)
|
||||
}
|
||||
u, err := m.client.PresignedGetObject(m.ctx, m.bucket, m.buildMinioPath(path), expires, reqParams)
|
||||
return u, convertMinioErr(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -59,11 +59,15 @@ type Object interface {
|
||||
// ObjectStorage represents an object storage to handle a bucket and files
|
||||
type ObjectStorage interface {
|
||||
Open(path string) (Object, error)
|
||||
// Save store a object, if size is unknown set -1
|
||||
|
||||
// Save store an object, if size is unknown set -1
|
||||
// NOTICE: Some storage SDK will close the Reader after saving if it is also a Closer,
|
||||
// DO NOT use the reader anymore after Save, or wrap it to a non-Closer reader.
|
||||
Save(path string, r io.Reader, size int64) (int64, error)
|
||||
|
||||
Stat(path string) (os.FileInfo, error)
|
||||
Delete(path string) error
|
||||
URL(path, name string, reqParams url.Values) (*url.URL, error)
|
||||
URL(path, name, method string, reqParams url.Values) (*url.URL, error)
|
||||
IterateObjects(path string, iterator func(path string, obj Object) error) error
|
||||
}
|
||||
|
||||
|
||||
@@ -262,7 +262,8 @@ func (it IssueTemplate) Type() IssueTemplateType {
|
||||
// IssueMeta basic issue information
|
||||
// swagger:model
|
||||
type IssueMeta struct {
|
||||
Index int64 `json:"index"`
|
||||
Index int64 `json:"index"`
|
||||
// owner of the issue's repo
|
||||
Owner string `json:"owner"`
|
||||
Name string `json:"repo"`
|
||||
}
|
||||
|
||||
+14
-15
@@ -48,15 +48,16 @@ type ExternalWiki struct {
|
||||
|
||||
// Repository represents a repository
|
||||
type Repository struct {
|
||||
ID int64 `json:"id"`
|
||||
Owner *User `json:"owner"`
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"full_name"`
|
||||
Description string `json:"description"`
|
||||
Empty bool `json:"empty"`
|
||||
Private bool `json:"private"`
|
||||
Fork bool `json:"fork"`
|
||||
Template bool `json:"template"`
|
||||
ID int64 `json:"id"`
|
||||
Owner *User `json:"owner"`
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"full_name"`
|
||||
Description string `json:"description"`
|
||||
Empty bool `json:"empty"`
|
||||
Private bool `json:"private"`
|
||||
Fork bool `json:"fork"`
|
||||
Template bool `json:"template"`
|
||||
// the original repository if this repository is a fork, otherwise null
|
||||
Parent *Repository `json:"parent,omitempty"`
|
||||
Mirror bool `json:"mirror"`
|
||||
Size int `json:"size"`
|
||||
@@ -225,15 +226,13 @@ type EditRepoOption struct {
|
||||
EnablePrune *bool `json:"enable_prune,omitempty"`
|
||||
}
|
||||
|
||||
// GenerateRepoOption options when creating repository using a template
|
||||
// GenerateRepoOption options when creating a repository using a template
|
||||
// swagger:model
|
||||
type GenerateRepoOption struct {
|
||||
// The organization or person who will own the new repository
|
||||
// the organization's name or individual user's name who will own the new repository
|
||||
//
|
||||
// required: true
|
||||
Owner string `json:"owner"`
|
||||
// Name of the repository to create
|
||||
//
|
||||
// required: true
|
||||
// unique: true
|
||||
Name string `json:"name" binding:"Required;AlphaDashDot;MaxSize(100)"`
|
||||
@@ -352,9 +351,9 @@ func (gt GitServiceType) Title() string {
|
||||
type MigrateRepoOptions struct {
|
||||
// required: true
|
||||
CloneAddr string `json:"clone_addr" binding:"Required"`
|
||||
// deprecated (only for backwards compatibility)
|
||||
// deprecated (only for backwards compatibility, use repo_owner instead)
|
||||
RepoOwnerID int64 `json:"uid"`
|
||||
// Name of User or Organisation who will own Repo after migration
|
||||
// the organization's name or individual user's name who will own the migrated repository
|
||||
RepoOwner string `json:"repo_owner"`
|
||||
// required: true
|
||||
RepoName string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"`
|
||||
|
||||
@@ -19,7 +19,7 @@ func IsLikelyEllipsisLeftPart(s string) bool {
|
||||
return strings.HasSuffix(s, utf8Ellipsis) || strings.HasSuffix(s, asciiEllipsis)
|
||||
}
|
||||
|
||||
func ellipsisGuessDisplayWidth(r rune) int {
|
||||
func ellipsisDisplayGuessWidth(r rune) int {
|
||||
// To make the truncated string as long as possible,
|
||||
// CJK/emoji chars are considered as 2-ASCII width but not 3-4 bytes width.
|
||||
// Here we only make the best guess (better than counting them in bytes),
|
||||
@@ -48,13 +48,17 @@ func ellipsisGuessDisplayWidth(r rune) int {
|
||||
// It appends "…" or "..." at the end of truncated string.
|
||||
// It guarantees the length of the returned runes doesn't exceed the limit.
|
||||
func EllipsisDisplayString(str string, limit int) string {
|
||||
s, _, _, _ := ellipsisDisplayString(str, limit)
|
||||
s, _, _, _ := ellipsisDisplayString(str, limit, ellipsisDisplayGuessWidth)
|
||||
return s
|
||||
}
|
||||
|
||||
// EllipsisDisplayStringX works like EllipsisDisplayString while it also returns the right part
|
||||
func EllipsisDisplayStringX(str string, limit int) (left, right string) {
|
||||
left, offset, truncated, encounterInvalid := ellipsisDisplayString(str, limit)
|
||||
return ellipsisDisplayStringX(str, limit, ellipsisDisplayGuessWidth)
|
||||
}
|
||||
|
||||
func ellipsisDisplayStringX(str string, limit int, widthGuess func(rune) int) (left, right string) {
|
||||
left, offset, truncated, encounterInvalid := ellipsisDisplayString(str, limit, widthGuess)
|
||||
if truncated {
|
||||
right = str[offset:]
|
||||
r, _ := utf8.DecodeRune(UnsafeStringToBytes(right))
|
||||
@@ -68,7 +72,7 @@ func EllipsisDisplayStringX(str string, limit int) (left, right string) {
|
||||
return left, right
|
||||
}
|
||||
|
||||
func ellipsisDisplayString(str string, limit int) (res string, offset int, truncated, encounterInvalid bool) {
|
||||
func ellipsisDisplayString(str string, limit int, widthGuess func(rune) int) (res string, offset int, truncated, encounterInvalid bool) {
|
||||
if len(str) <= limit {
|
||||
return str, len(str), false, false
|
||||
}
|
||||
@@ -81,7 +85,7 @@ func ellipsisDisplayString(str string, limit int) (res string, offset int, trunc
|
||||
for i, r := range str {
|
||||
encounterInvalid = encounterInvalid || r == utf8.RuneError
|
||||
pos = i
|
||||
runeWidth := ellipsisGuessDisplayWidth(r)
|
||||
runeWidth := widthGuess(r)
|
||||
if used+runeWidth+3 > limit {
|
||||
break
|
||||
}
|
||||
@@ -96,7 +100,7 @@ func ellipsisDisplayString(str string, limit int) (res string, offset int, trunc
|
||||
if nextCnt >= 4 {
|
||||
break
|
||||
}
|
||||
nextWidth += ellipsisGuessDisplayWidth(r)
|
||||
nextWidth += widthGuess(r)
|
||||
nextCnt++
|
||||
}
|
||||
if nextCnt <= 3 && used+nextWidth <= limit {
|
||||
@@ -114,6 +118,10 @@ func ellipsisDisplayString(str string, limit int) (res string, offset int, trunc
|
||||
return str[:offset] + ellipsis, offset, true, encounterInvalid
|
||||
}
|
||||
|
||||
func EllipsisTruncateRunes(str string, limit int) (left, right string) {
|
||||
return ellipsisDisplayStringX(str, limit, func(r rune) int { return 1 })
|
||||
}
|
||||
|
||||
// TruncateRunes returns a truncated string with given rune limit,
|
||||
// it returns input string if its rune length doesn't exceed the limit.
|
||||
func TruncateRunes(str string, limit int) string {
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestEllipsisGuessDisplayWidth(t *testing.T) {
|
||||
t.Run(c.r, func(t *testing.T) {
|
||||
w := 0
|
||||
for _, r := range c.r {
|
||||
w += ellipsisGuessDisplayWidth(r)
|
||||
w += ellipsisDisplayGuessWidth(r)
|
||||
}
|
||||
assert.Equal(t, c.want, w, "hex=% x", []byte(c.r))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user