mirror of
https://github.com/go-gitea/gitea
synced 2026-02-06 20:33:01 +00:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68cceb5321 | ||
|
|
15b61dac98 | ||
|
|
35ca651c80 | ||
|
|
737486152c | ||
|
|
c40c753613 | ||
|
|
7a9b01a2dd | ||
|
|
b43d7e1254 | ||
|
|
987798a3a9 | ||
|
|
13b74accda | ||
|
|
79fa1c15a4 | ||
|
|
78dabdd9ae | ||
|
|
e5d2031828 | ||
|
|
c3b4f3f7e9 | ||
|
|
9bccfe9856 | ||
|
|
85034564c2 | ||
|
|
eacab6b10d | ||
|
|
ac9792c0c7 | ||
|
|
f7c874cb1a | ||
|
|
d19c2c9fcb | ||
|
|
59228d8a71 | ||
|
|
67701771af | ||
|
|
113d13a026 | ||
|
|
9ec1c8812e | ||
|
|
e1e43333cf | ||
|
|
cedf4fef0a | ||
|
|
a04fc567b4 | ||
|
|
92d79b556b | ||
|
|
65176fdaf3 | ||
|
|
aac905dcfb | ||
|
|
5ce8fdbc37 | ||
|
|
76accb51ed | ||
|
|
bd2218e14c | ||
|
|
0747592865 | ||
|
|
07d140625e | ||
|
|
a6c2a1a117 | ||
|
|
56b99551ae | ||
|
|
51c8c0f3fe |
@@ -4,6 +4,45 @@ This changelog goes through all the changes that have been made in each release
|
||||
without substantial changes to our git log; to see the highlights of what has
|
||||
been added to each release, please refer to the [blog](https://blog.gitea.io).
|
||||
|
||||
## [1.17.1](https://github.com/go-gitea/gitea/releases/tag/1.17.1) - 2022-08-17
|
||||
|
||||
* SECURITY
|
||||
* Correctly escape within tribute.js (#20831) (#20832)
|
||||
* ENHANCEMENTS
|
||||
* Add support for NuGet API keys (#20721) (#20734)
|
||||
* Display project in issue list (#20583)
|
||||
* Add disable download source configuration (#20548) (#20579)
|
||||
* Add username check to doctor (#20140) (#20671)
|
||||
* Enable Wire 2 for Internal SSH Server (#20616) (#20617)
|
||||
* BUGFIXES
|
||||
* Use the total issue count for UI (#20785) (#20827)
|
||||
* Add proxy host into allow list (#20798) (#20819)
|
||||
* Add missing translation for queue flush workers (#20791) (#20792)
|
||||
* Improve comment header for mobile (#20781) (#20789)
|
||||
* Fix git.Init for doctor sub-command (#20782) (#20783)
|
||||
* Check webhooks slice length before calling xorm (#20642) (#20768)
|
||||
* Remove manual rollback for failed generated repositories (#20639) (#20762)
|
||||
* Use correct field name in npm template (#20675) (#20760)
|
||||
* Keep download count on Container tag overwrite (#20728) (#20735)
|
||||
* Fix v220 migration to be compatible for MSSQL 2008 r2 (#20702) (#20707)
|
||||
* Use request timeout for git service rpc (#20689) (#20693)
|
||||
* Send correct NuGet status codes (#20647) (#20677)
|
||||
* Use correct context to get package content (#20673) (#20676)
|
||||
* Fix the JS error "EventSource is not defined" caused by some non-standard browsers (#20584) (#20663)
|
||||
* Add default commit messages to PR for squash merge (#20618) (#20645)
|
||||
* Fix package upload for files >32mb (#20622) (#20635)
|
||||
* Fix the new-line copy-paste for rendered code (#20612)
|
||||
* Clean up and fix clone button script (#20415 & #20600) (#20599)
|
||||
* Fix default merge style (#20564) (#20565)
|
||||
* Add repository condition for issue count (#20454) (#20496)
|
||||
* Make branch icon stand out more (#20726) (#20774)
|
||||
* Fix loading button with invalid form (#20754) (#20759)
|
||||
* Fix SecToTime edge-cases (#20610) (#20611)
|
||||
* Executable check always returns true for windows (#20637) (#20835)
|
||||
* Check issue labels slice length before calling xorm Insert (#20655) (#20836)
|
||||
* Fix owners cannot create organization repos bug (#20841) (#20854)
|
||||
* Prevent 500 is head repo does not have PullRequest unit in IsUserAllowedToUpdate (#20839) (#20848)
|
||||
|
||||
## [1.17.0](https://github.com/go-gitea/gitea/releases/tag/v1.17.0) - 2022-07-30
|
||||
|
||||
* BREAKING
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
func init() {
|
||||
setting.SetCustomPathAndConf("", "", "")
|
||||
setting.LoadForTest()
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m, &unittest.TestOptions{
|
||||
GiteaRootPath: "..",
|
||||
})
|
||||
}
|
||||
+50
-37
@@ -12,9 +12,11 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
"code.gitea.io/gitea/models/migrations"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
|
||||
@@ -25,13 +27,13 @@ import (
|
||||
var CmdMigrateStorage = cli.Command{
|
||||
Name: "migrate-storage",
|
||||
Usage: "Migrate the storage",
|
||||
Description: "This is a command for migrating storage.",
|
||||
Description: "Copies stored files from storage configured in app.ini to parameter-configured storage",
|
||||
Action: runMigrateStorage,
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "type, t",
|
||||
Value: "",
|
||||
Usage: "Kinds of files to migrate, currently only 'attachments' is supported",
|
||||
Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages'",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "storage, s",
|
||||
@@ -80,34 +82,53 @@ var CmdMigrateStorage = cli.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func migrateAttachments(dstStorage storage.ObjectStorage) error {
|
||||
return repo_model.IterateAttachment(func(attach *repo_model.Attachment) error {
|
||||
func migrateAttachments(ctx context.Context, dstStorage storage.ObjectStorage) error {
|
||||
return db.IterateObjects(ctx, func(attach *repo_model.Attachment) error {
|
||||
_, err := storage.Copy(dstStorage, attach.RelativePath(), storage.Attachments, attach.RelativePath())
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func migrateLFS(dstStorage storage.ObjectStorage) error {
|
||||
return git_model.IterateLFS(func(mo *git_model.LFSMetaObject) error {
|
||||
func migrateLFS(ctx context.Context, dstStorage storage.ObjectStorage) error {
|
||||
return db.IterateObjects(ctx, func(mo *git_model.LFSMetaObject) error {
|
||||
_, err := storage.Copy(dstStorage, mo.RelativePath(), storage.LFS, mo.RelativePath())
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func migrateAvatars(dstStorage storage.ObjectStorage) error {
|
||||
return user_model.IterateUser(func(user *user_model.User) error {
|
||||
func migrateAvatars(ctx context.Context, dstStorage storage.ObjectStorage) error {
|
||||
return db.IterateObjects(ctx, func(user *user_model.User) error {
|
||||
_, err := storage.Copy(dstStorage, user.CustomAvatarRelativePath(), storage.Avatars, user.CustomAvatarRelativePath())
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func migrateRepoAvatars(dstStorage storage.ObjectStorage) error {
|
||||
return repo_model.IterateRepository(func(repo *repo_model.Repository) error {
|
||||
func migrateRepoAvatars(ctx context.Context, dstStorage storage.ObjectStorage) error {
|
||||
return db.IterateObjects(ctx, func(repo *repo_model.Repository) error {
|
||||
_, err := storage.Copy(dstStorage, repo.CustomAvatarRelativePath(), storage.RepoAvatars, repo.CustomAvatarRelativePath())
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func migrateRepoArchivers(ctx context.Context, dstStorage storage.ObjectStorage) error {
|
||||
return db.IterateObjects(ctx, func(archiver *repo_model.RepoArchiver) error {
|
||||
p, err := archiver.RelativePath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = storage.Copy(dstStorage, p, storage.RepoArchives, p)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func migratePackages(ctx context.Context, dstStorage storage.ObjectStorage) error {
|
||||
return db.IterateObjects(ctx, func(pb *packages_model.PackageBlob) error {
|
||||
p := packages_module.KeyToRelativePath(packages_module.BlobHash256Key(pb.HashSHA256))
|
||||
_, err := storage.Copy(dstStorage, p, storage.Packages, p)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func runMigrateStorage(ctx *cli.Context) error {
|
||||
stdCtx, cancel := installSignals()
|
||||
defer cancel()
|
||||
@@ -127,8 +148,6 @@ func runMigrateStorage(ctx *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
goCtx := context.Background()
|
||||
|
||||
if err := storage.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -145,13 +164,13 @@ func runMigrateStorage(ctx *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
dstStorage, err = storage.NewLocalStorage(
|
||||
goCtx,
|
||||
stdCtx,
|
||||
storage.LocalStorageConfig{
|
||||
Path: p,
|
||||
})
|
||||
case string(storage.MinioStorageType):
|
||||
dstStorage, err = storage.NewMinioStorage(
|
||||
goCtx,
|
||||
stdCtx,
|
||||
storage.MinioStorageConfig{
|
||||
Endpoint: ctx.String("minio-endpoint"),
|
||||
AccessKeyID: ctx.String("minio-access-key-id"),
|
||||
@@ -162,35 +181,29 @@ func runMigrateStorage(ctx *cli.Context) error {
|
||||
UseSSL: ctx.Bool("minio-use-ssl"),
|
||||
})
|
||||
default:
|
||||
return fmt.Errorf("Unsupported storage type: %s", ctx.String("storage"))
|
||||
return fmt.Errorf("unsupported storage type: %s", ctx.String("storage"))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tp := strings.ToLower(ctx.String("type"))
|
||||
switch tp {
|
||||
case "attachments":
|
||||
if err := migrateAttachments(dstStorage); err != nil {
|
||||
return err
|
||||
}
|
||||
case "lfs":
|
||||
if err := migrateLFS(dstStorage); err != nil {
|
||||
return err
|
||||
}
|
||||
case "avatars":
|
||||
if err := migrateAvatars(dstStorage); err != nil {
|
||||
return err
|
||||
}
|
||||
case "repo-avatars":
|
||||
if err := migrateRepoAvatars(dstStorage); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("Unsupported storage: %s", ctx.String("type"))
|
||||
migratedMethods := map[string]func(context.Context, storage.ObjectStorage) error{
|
||||
"attachments": migrateAttachments,
|
||||
"lfs": migrateLFS,
|
||||
"avatars": migrateAvatars,
|
||||
"repo-avatars": migrateRepoAvatars,
|
||||
"repo-archivers": migrateRepoArchivers,
|
||||
"packages": migratePackages,
|
||||
}
|
||||
|
||||
log.Warn("All files have been copied to the new placement but old files are still on the original placement.")
|
||||
tp := strings.ToLower(ctx.String("type"))
|
||||
if m, ok := migratedMethods[tp]; ok {
|
||||
if err := m(stdCtx, dstStorage); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info("%s files have successfully been copied to the new storage.", tp)
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
return fmt.Errorf("unsupported storage: %s", ctx.String("type"))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMigratePackages(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
creator := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
|
||||
|
||||
content := "package main\n\nfunc main() {\nfmt.Println(\"hi\")\n}\n"
|
||||
buf, err := packages_module.CreateHashedBufferFromReader(strings.NewReader(content), 1024)
|
||||
assert.NoError(t, err)
|
||||
defer buf.Close()
|
||||
|
||||
v, f, err := packages_service.CreatePackageAndAddFile(&packages_service.PackageCreationInfo{
|
||||
PackageInfo: packages_service.PackageInfo{
|
||||
Owner: creator,
|
||||
PackageType: packages.TypeGeneric,
|
||||
Name: "test",
|
||||
Version: "1.0.0",
|
||||
},
|
||||
Creator: creator,
|
||||
SemverCompatible: true,
|
||||
VersionProperties: map[string]string{},
|
||||
}, &packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: "a.go",
|
||||
},
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, v)
|
||||
assert.NotNil(t, f)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
p, err := os.MkdirTemp(os.TempDir(), "migrated_packages")
|
||||
assert.NoError(t, err)
|
||||
|
||||
dstStorage, err := storage.NewLocalStorage(
|
||||
ctx,
|
||||
storage.LocalStorageConfig{
|
||||
Path: p,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = migratePackages(ctx, dstStorage)
|
||||
assert.NoError(t, err)
|
||||
|
||||
entries, err := os.ReadDir(p)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 2, len(entries))
|
||||
assert.EqualValues(t, "01", entries[0].Name())
|
||||
assert.EqualValues(t, "tmp", entries[1].Name())
|
||||
}
|
||||
@@ -892,6 +892,9 @@ ROUTER = console
|
||||
;; Allow deletion of unadopted repositories
|
||||
;ALLOW_DELETION_OF_UNADOPTED_REPOSITORIES = false
|
||||
|
||||
;; Don't allow download source archive files from UI
|
||||
;DISABLE_DOWNLOAD_SOURCE_ARCHIVES = false
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;[repository.editor]
|
||||
|
||||
@@ -78,6 +78,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
|
||||
- `DEFAULT_BRANCH`: **main**: Default branch name of all repositories.
|
||||
- `ALLOW_ADOPTION_OF_UNADOPTED_REPOSITORIES`: **false**: Allow non-admin users to adopt unadopted repositories
|
||||
- `ALLOW_DELETION_OF_UNADOPTED_REPOSITORIES`: **false**: Allow non-admin users to delete unadopted repositories
|
||||
- `DISABLE_DOWNLOAD_SOURCE_ARCHIVES`: **false**: Don't allow download source archive files from UI
|
||||
|
||||
### Repository - Editor (`repository.editor`)
|
||||
|
||||
|
||||
@@ -47,6 +47,8 @@ For example:
|
||||
dotnet nuget add source --name gitea --username testuser --password password123 https://gitea.example.com/api/packages/testuser/nuget/index.json
|
||||
```
|
||||
|
||||
You can add the source without credentials and use the [`--api-key`](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-nuget-push) parameter when publishing packages. In this case you need to provide a [personal access token]({{< relref "doc/developers/api-usage.en-us.md#authentication" >}}).
|
||||
|
||||
## Publish a package
|
||||
|
||||
Publish a package by running the following command:
|
||||
|
||||
@@ -275,11 +275,23 @@ func TestPackageContainer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Overwrite existing tag
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/manifests/%s", url, tag))
|
||||
addTokenAuthHeader(req, userToken)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
pv, err = packages_model.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, image, tag)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, pv.DownloadCount)
|
||||
|
||||
// Overwrite existing tag should keep the download count
|
||||
req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, tag), strings.NewReader(manifestContent))
|
||||
addTokenAuthHeader(req, userToken)
|
||||
req.Header.Set("Content-Type", oci.MediaTypeDockerManifest)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
pv, err = packages_model.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, image, tag)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, pv.DownloadCount)
|
||||
})
|
||||
|
||||
t.Run("HeadManifest", func(t *testing.T) {
|
||||
|
||||
@@ -24,9 +24,16 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func addNuGetAPIKeyHeader(request *http.Request, token string) *http.Request {
|
||||
request.Header.Set("X-NuGet-ApiKey", token)
|
||||
return request
|
||||
}
|
||||
|
||||
func TestPackageNuGet(t *testing.T) {
|
||||
defer prepareTestEnv(t)()
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
|
||||
token := getUserToken(t, user.Name)
|
||||
|
||||
packageName := "test.package"
|
||||
packageVersion := "1.0.3"
|
||||
@@ -60,6 +67,10 @@ func TestPackageNuGet(t *testing.T) {
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("%s/index.json", url))
|
||||
req = AddBasicAuthHeader(req, user.Name)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/index.json", url))
|
||||
req = addNuGetAPIKeyHeader(req, token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var result nuget.ServiceIndexResponse
|
||||
@@ -122,7 +133,7 @@ func TestPackageNuGet(t *testing.T) {
|
||||
|
||||
req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content))
|
||||
req = AddBasicAuthHeader(req, user.Name)
|
||||
MakeRequest(t, req, http.StatusBadRequest)
|
||||
MakeRequest(t, req, http.StatusConflict)
|
||||
})
|
||||
|
||||
t.Run("SymbolPackage", func(t *testing.T) {
|
||||
@@ -208,7 +219,7 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
|
||||
|
||||
req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createPackage(packageName, "SymbolsPackage"))
|
||||
req = AddBasicAuthHeader(req, user.Name)
|
||||
MakeRequest(t, req, http.StatusBadRequest)
|
||||
MakeRequest(t, req, http.StatusConflict)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -352,7 +363,7 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
|
||||
|
||||
req := NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s", url, packageName, packageVersion))
|
||||
req = AddBasicAuthHeader(req, user.Name)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNuGet)
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// IterateObjects iterate all the Bean object
|
||||
func IterateObjects[Object any](ctx context.Context, f func(repo *Object) error) error {
|
||||
var start int
|
||||
batchSize := setting.Database.IterateBufferSize
|
||||
sess := GetEngine(ctx)
|
||||
for {
|
||||
repos := make([]*Object, 0, batchSize)
|
||||
if err := sess.Limit(batchSize, start).Find(&repos); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(repos) == 0 {
|
||||
return nil
|
||||
}
|
||||
start += len(repos)
|
||||
|
||||
for _, repo := range repos {
|
||||
if err := f(repo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -278,29 +278,6 @@ func LFSAutoAssociate(metas []*LFSMetaObject, user *user_model.User, repoID int6
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// IterateLFS iterates lfs object
|
||||
func IterateLFS(f func(mo *LFSMetaObject) error) error {
|
||||
var start int
|
||||
const batchSize = 100
|
||||
e := db.GetEngine(db.DefaultContext)
|
||||
for {
|
||||
mos := make([]*LFSMetaObject, 0, batchSize)
|
||||
if err := e.Limit(batchSize, start).Find(&mos); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(mos) == 0 {
|
||||
return nil
|
||||
}
|
||||
start += len(mos)
|
||||
|
||||
for _, mo := range mos {
|
||||
if err := f(mo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CopyLFS copies LFS data from one repo to another
|
||||
func CopyLFS(ctx context.Context, newRepo, oldRepo *repo_model.Repository) error {
|
||||
var lfsObjects []*LFSMetaObject
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
@@ -222,6 +223,46 @@ func (issues IssueList) loadMilestones(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issues IssueList) getProjectIDs() []int64 {
|
||||
ids := make(map[int64]struct{}, len(issues))
|
||||
for _, issue := range issues {
|
||||
projectID := issue.ProjectID()
|
||||
if _, ok := ids[projectID]; !ok {
|
||||
ids[projectID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return container.KeysInt64(ids)
|
||||
}
|
||||
|
||||
func (issues IssueList) loadProjects(ctx context.Context) error {
|
||||
projectIDs := issues.getProjectIDs()
|
||||
if len(projectIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
projectMaps := make(map[int64]*project_model.Project, len(projectIDs))
|
||||
left := len(projectIDs)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
err := db.GetEngine(ctx).
|
||||
In("id", projectIDs[:limit]).
|
||||
Find(&projectMaps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
left -= limit
|
||||
projectIDs = projectIDs[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
issue.Project = projectMaps[issue.ProjectID()]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issues IssueList) loadAssignees(ctx context.Context) error {
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
@@ -495,6 +536,10 @@ func (issues IssueList) loadAttributes(ctx context.Context) error {
|
||||
return fmt.Errorf("issue.loadAttributes: loadMilestones: %v", err)
|
||||
}
|
||||
|
||||
if err := issues.loadProjects(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadProjects: %v", err)
|
||||
}
|
||||
|
||||
if err := issues.loadAssignees(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadAssignees: %v", err)
|
||||
}
|
||||
|
||||
@@ -12,18 +12,17 @@ import (
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
func addContainerRepositoryProperty(x *xorm.Engine) error {
|
||||
func addContainerRepositoryProperty(x *xorm.Engine) (err error) {
|
||||
switch x.Dialect().URI().DBType {
|
||||
case schemas.SQLITE:
|
||||
_, err := x.Exec("INSERT INTO package_property (ref_type, ref_id, name, value) SELECT ?, p.id, ?, u.lower_name || '/' || p.lower_name FROM package p JOIN `user` u ON p.owner_id = u.id WHERE p.type = ?", packages_model.PropertyTypePackage, container_module.PropertyRepository, packages_model.TypeContainer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = x.Exec("INSERT INTO package_property (ref_type, ref_id, name, value) SELECT ?, p.id, ?, u.lower_name || '/' || p.lower_name FROM package p JOIN `user` u ON p.owner_id = u.id WHERE p.type = ?",
|
||||
packages_model.PropertyTypePackage, container_module.PropertyRepository, packages_model.TypeContainer)
|
||||
case schemas.MSSQL:
|
||||
_, err = x.Exec("INSERT INTO package_property (ref_type, ref_id, name, value) SELECT ?, p.id, ?, u.lower_name + '/' + p.lower_name FROM package p JOIN `user` u ON p.owner_id = u.id WHERE p.type = ?",
|
||||
packages_model.PropertyTypePackage, container_module.PropertyRepository, packages_model.TypeContainer)
|
||||
default:
|
||||
_, err := x.Exec("INSERT INTO package_property (ref_type, ref_id, name, value) SELECT ?, p.id, ?, CONCAT(u.lower_name, '/', p.lower_name) FROM package p JOIN `user` u ON p.owner_id = u.id WHERE p.type = ?", packages_model.PropertyTypePackage, container_module.PropertyRepository, packages_model.TypeContainer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = x.Exec("INSERT INTO package_property (ref_type, ref_id, name, value) SELECT ?, p.id, ?, CONCAT(u.lower_name, '/', p.lower_name) FROM package p JOIN `user` u ON p.owner_id = u.id WHERE p.type = ?",
|
||||
packages_model.PropertyTypePackage, container_module.PropertyRepository, packages_model.TypeContainer)
|
||||
}
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -226,28 +226,6 @@ func DeleteAttachmentsByRelease(releaseID int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// IterateAttachment iterates attachments; it should not be used when Gitea is servicing users.
|
||||
func IterateAttachment(f func(attach *Attachment) error) error {
|
||||
var start int
|
||||
const batchSize = 100
|
||||
for {
|
||||
attachments := make([]*Attachment, 0, batchSize)
|
||||
if err := db.GetEngine(db.DefaultContext).Limit(batchSize, start).Find(&attachments); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(attachments) == 0 {
|
||||
return nil
|
||||
}
|
||||
start += len(attachments)
|
||||
|
||||
for _, attach := range attachments {
|
||||
if err := f(attach); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CountOrphanedAttachments returns the number of bad attachments
|
||||
func CountOrphanedAttachments() (int64, error) {
|
||||
return db.GetEngine(db.DefaultContext).Where("(issue_id > 0 and issue_id not in (select id from issue)) or (release_id > 0 and release_id not in (select id from `release`))").
|
||||
|
||||
@@ -153,7 +153,9 @@ func (repos MirrorRepositoryList) loadAttributes(ctx context.Context) error {
|
||||
}
|
||||
for i := range repos {
|
||||
repos[i].Mirror = set[repos[i].ID]
|
||||
repos[i].Mirror.Repo = repos[i]
|
||||
if repos[i].Mirror != nil {
|
||||
repos[i].Mirror.Repo = repos[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -14,36 +14,12 @@ import (
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// IterateRepository iterate repositories
|
||||
func IterateRepository(f func(repo *Repository) error) error {
|
||||
var start int
|
||||
batchSize := setting.Database.IterateBufferSize
|
||||
sess := db.GetEngine(db.DefaultContext)
|
||||
for {
|
||||
repos := make([]*Repository, 0, batchSize)
|
||||
if err := sess.Limit(batchSize, start).Find(&repos); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(repos) == 0 {
|
||||
return nil
|
||||
}
|
||||
start += len(repos)
|
||||
|
||||
for _, repo := range repos {
|
||||
if err := f(repo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FindReposMapByIDs find repos as map
|
||||
func FindReposMapByIDs(repoIDs []int64, res map[int64]*Repository) error {
|
||||
return db.GetEngine(db.DefaultContext).In("id", repoIDs).Find(&res)
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
@@ -125,28 +124,6 @@ func SearchUsers(opts *SearchUserOptions) (users []*User, _ int64, _ error) {
|
||||
return users, count, sessQuery.Find(&users)
|
||||
}
|
||||
|
||||
// IterateUser iterate users
|
||||
func IterateUser(f func(user *User) error) error {
|
||||
var start int
|
||||
batchSize := setting.Database.IterateBufferSize
|
||||
for {
|
||||
users := make([]*User, 0, batchSize)
|
||||
if err := db.GetEngine(db.DefaultContext).Limit(batchSize, start).Find(&users); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(users) == 0 {
|
||||
return nil
|
||||
}
|
||||
start += len(users)
|
||||
|
||||
for _, user := range users {
|
||||
if err := f(user); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BuildCanSeeUserCondition creates a condition which can be used to restrict results to users/orgs the actor can see
|
||||
func BuildCanSeeUserCondition(actor *User) builder.Cond {
|
||||
if actor != nil {
|
||||
|
||||
@@ -399,6 +399,10 @@ func CreateWebhook(ctx context.Context, w *Webhook) error {
|
||||
|
||||
// CreateWebhooks creates multiple web hooks
|
||||
func CreateWebhooks(ctx context.Context, ws []*Webhook) error {
|
||||
// xorm returns err "no element on slice when insert" for empty slices.
|
||||
if len(ws) == 0 {
|
||||
return nil
|
||||
}
|
||||
for i := 0; i < len(ws); i++ {
|
||||
ws[i].Type = strings.TrimSpace(ws[i].Type)
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ func (ctx *Context) HTML(status int, name base.TplName) {
|
||||
ctx.Data["TemplateLoadTimes"] = func() string {
|
||||
return strconv.FormatInt(time.Since(tmplStartTime).Nanoseconds()/1e6, 10) + "ms"
|
||||
}
|
||||
if err := ctx.Render.HTML(ctx.Resp, status, string(name), ctx.Data); err != nil {
|
||||
if err := ctx.Render.HTML(ctx.Resp, status, string(name), templates.BaseVars().Merge(ctx.Data)); err != nil {
|
||||
if status == http.StatusInternalServerError && name == base.TplName("status/500") {
|
||||
ctx.PlainText(http.StatusInternalServerError, "Unable to find status/500 template")
|
||||
return
|
||||
|
||||
@@ -58,6 +58,29 @@ func checkUserEmail(ctx context.Context, logger log.Logger, _ bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// From time to time Gitea makes changes to the reserved usernames and which symbols
|
||||
// are allowed for various reasons. This check helps with detecting users that, according
|
||||
// to our reserved names, don't have a valid username.
|
||||
func checkUserName(ctx context.Context, logger log.Logger, _ bool) error {
|
||||
var invalidUserCount int64
|
||||
if err := iterateUserAccounts(ctx, func(u *user.User) error {
|
||||
if err := user.IsUsableUsername(u.Name); err != nil {
|
||||
invalidUserCount++
|
||||
logger.Warn("User[id=%d] does not have a valid username: %v", u.ID, err)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return fmt.Errorf("iterateUserAccounts: %v", err)
|
||||
}
|
||||
|
||||
if invalidUserCount == 0 {
|
||||
logger.Info("All users have a valid username.")
|
||||
} else {
|
||||
logger.Warn("%d user(s) have a non-valid username.", invalidUserCount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
Register(&Check{
|
||||
Title: "Check if users has an valid email address",
|
||||
@@ -66,4 +89,11 @@ func init() {
|
||||
Run: checkUserEmail,
|
||||
Priority: 9,
|
||||
})
|
||||
Register(&Check{
|
||||
Title: "Check if users have a valid username",
|
||||
Name: "check-user-names",
|
||||
IsDefault: false,
|
||||
Run: checkUserName,
|
||||
Priority: 9,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
@@ -49,7 +50,11 @@ func initDBDisableConsole(ctx context.Context, disableConsole bool) error {
|
||||
|
||||
setting.NewXORMLogService(disableConsole)
|
||||
if err := db.InitEngine(ctx); err != nil {
|
||||
return fmt.Errorf("models.SetEngine: %v", err)
|
||||
return fmt.Errorf("db.InitEngine: %w", err)
|
||||
}
|
||||
// some doctor sub-commands need to use git command
|
||||
if err := git.InitOnceWithSync(ctx); err != nil {
|
||||
return fmt.Errorf("git.InitOnceWithSync: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+17
-8
@@ -95,14 +95,15 @@ func (c *Command) AddArguments(args ...string) *Command {
|
||||
return c
|
||||
}
|
||||
|
||||
// RunOpts represents parameters to run the command
|
||||
// RunOpts represents parameters to run the command. If UseContextTimeout is specified, then Timeout is ignored.
|
||||
type RunOpts struct {
|
||||
Env []string
|
||||
Timeout time.Duration
|
||||
Dir string
|
||||
Stdout, Stderr io.Writer
|
||||
Stdin io.Reader
|
||||
PipelineFunc func(context.Context, context.CancelFunc) error
|
||||
Env []string
|
||||
Timeout time.Duration
|
||||
UseContextTimeout bool
|
||||
Dir string
|
||||
Stdout, Stderr io.Writer
|
||||
Stdin io.Reader
|
||||
PipelineFunc func(context.Context, context.CancelFunc) error
|
||||
}
|
||||
|
||||
func commonBaseEnvs() []string {
|
||||
@@ -171,7 +172,15 @@ func (c *Command) Run(opts *RunOpts) error {
|
||||
desc = fmt.Sprintf("%s %s [repo_path: %s]", c.name, strings.Join(args, " "), opts.Dir)
|
||||
}
|
||||
|
||||
ctx, cancel, finished := process.GetManager().AddContextTimeout(c.parentContext, opts.Timeout, desc)
|
||||
var ctx context.Context
|
||||
var cancel context.CancelFunc
|
||||
var finished context.CancelFunc
|
||||
|
||||
if opts.UseContextTimeout {
|
||||
ctx, cancel, finished = process.GetManager().AddContext(c.parentContext, desc)
|
||||
} else {
|
||||
ctx, cancel, finished = process.GetManager().AddContextTimeout(c.parentContext, opts.Timeout, desc)
|
||||
}
|
||||
defer finished()
|
||||
|
||||
cmd := exec.CommandContext(ctx, c.name, c.args...)
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
)
|
||||
|
||||
// don't index files larger than this many bytes for performance purposes
|
||||
const sizeLimit = 1000000
|
||||
const sizeLimit = 1024 * 1024
|
||||
|
||||
var (
|
||||
// For custom user mapping
|
||||
@@ -58,7 +58,7 @@ func NewContext() {
|
||||
func Code(fileName, language, code string) string {
|
||||
NewContext()
|
||||
|
||||
// diff view newline will be passed as empty, change to literal \n so it can be copied
|
||||
// diff view newline will be passed as empty, change to literal '\n' so it can be copied
|
||||
// preserve literal newline in blame view
|
||||
if code == "" || code == "\n" {
|
||||
return "\n"
|
||||
@@ -104,6 +104,11 @@ func Code(fileName, language, code string) string {
|
||||
return CodeFromLexer(lexer, code)
|
||||
}
|
||||
|
||||
type nopPreWrapper struct{}
|
||||
|
||||
func (nopPreWrapper) Start(code bool, styleAttr string) string { return "" }
|
||||
func (nopPreWrapper) End(code bool) string { return "" }
|
||||
|
||||
// CodeFromLexer returns a HTML version of code string with chroma syntax highlighting classes
|
||||
func CodeFromLexer(lexer chroma.Lexer, code string) string {
|
||||
formatter := html.New(html.WithClasses(true),
|
||||
@@ -126,9 +131,9 @@ func CodeFromLexer(lexer chroma.Lexer, code string) string {
|
||||
return code
|
||||
}
|
||||
|
||||
htmlw.Flush()
|
||||
_ = htmlw.Flush()
|
||||
// Chroma will add newlines for certain lexers in order to highlight them properly
|
||||
// Once highlighted, strip them here so they don't cause copy/paste trouble in HTML output
|
||||
// Once highlighted, strip them here, so they don't cause copy/paste trouble in HTML output
|
||||
return strings.TrimSuffix(htmlbuf.String(), "\n")
|
||||
}
|
||||
|
||||
@@ -141,7 +146,7 @@ func File(numLines int, fileName, language string, code []byte) []string {
|
||||
}
|
||||
formatter := html.New(html.WithClasses(true),
|
||||
html.WithLineNumbers(false),
|
||||
html.PreventSurroundingPre(true),
|
||||
html.WithPreWrapper(nopPreWrapper{}),
|
||||
)
|
||||
|
||||
if formatter == nil {
|
||||
@@ -189,27 +194,19 @@ func File(numLines int, fileName, language string, code []byte) []string {
|
||||
return plainText(string(code), numLines)
|
||||
}
|
||||
|
||||
htmlw.Flush()
|
||||
_ = htmlw.Flush()
|
||||
finalNewLine := false
|
||||
if len(code) > 0 {
|
||||
finalNewLine = code[len(code)-1] == '\n'
|
||||
}
|
||||
|
||||
m := make([]string, 0, numLines)
|
||||
for _, v := range strings.SplitN(htmlbuf.String(), "\n", numLines) {
|
||||
content := string(v)
|
||||
// need to keep lines that are only \n so copy/paste works properly in browser
|
||||
if content == "" {
|
||||
content = "\n"
|
||||
} else if content == `</span><span class="w">` {
|
||||
content += "\n</span>"
|
||||
} else if content == `</span></span><span class="line"><span class="cl">` {
|
||||
content += "\n"
|
||||
}
|
||||
content = strings.TrimSuffix(content, `<span class="w">`)
|
||||
content = strings.TrimPrefix(content, `</span>`)
|
||||
m = append(m, content)
|
||||
m := strings.SplitN(htmlbuf.String(), `</span></span><span class="line"><span class="cl">`, numLines)
|
||||
if len(m) > 0 {
|
||||
m[0] = m[0][len(`<span class="line"><span class="cl">`):]
|
||||
last := m[len(m)-1]
|
||||
m[len(m)-1] = last[:len(last)-len(`</span></span>`)]
|
||||
}
|
||||
|
||||
if finalNewLine {
|
||||
m = append(m, "<span class=\"w\">\n</span>")
|
||||
}
|
||||
@@ -219,14 +216,14 @@ func File(numLines int, fileName, language string, code []byte) []string {
|
||||
|
||||
// return unhiglighted map
|
||||
func plainText(code string, numLines int) []string {
|
||||
m := make([]string, 0, numLines)
|
||||
for _, v := range strings.SplitN(string(code), "\n", numLines) {
|
||||
content := string(v)
|
||||
m := strings.SplitN(code, "\n", numLines)
|
||||
|
||||
for i, content := range m {
|
||||
// need to keep lines that are only \n so copy/paste works properly in browser
|
||||
if content == "" {
|
||||
content = "\n"
|
||||
}
|
||||
m = append(m, gohtml.EscapeString(content))
|
||||
m[i] = gohtml.EscapeString(content)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
@@ -43,18 +43,29 @@ func TestFile(t *testing.T) {
|
||||
- go test -v -race -coverprofile=coverage.txt -covermode=atomic
|
||||
`),
|
||||
want: util.Dedent(`
|
||||
<span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">pipeline</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">default</span>
|
||||
</span></span><span class="line"><span class="cl">
|
||||
</span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">steps</span><span class="p">:</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">test</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">golang:1.13</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">environment</span><span class="p">:</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"></span><span class="w"> </span><span class="nt">GOPROXY</span><span class="p">:</span><span class="w"> </span><span class="l">https://goproxy.cn</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">commands</span><span class="p">:</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"></span><span class="w"> </span>- <span class="l">go get -u</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">go build -v</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">go test -v -race -coverprofile=coverage.txt -covermode=atomic</span></span></span>
|
||||
<span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">pipeline</span><span class="w">
|
||||
</span>
|
||||
<span class="w"></span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
|
||||
</span>
|
||||
<span class="w">
|
||||
</span>
|
||||
<span class="w"></span><span class="nt">steps</span><span class="p">:</span><span class="w">
|
||||
</span>
|
||||
<span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">test</span><span class="w">
|
||||
</span>
|
||||
<span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">golang:1.13</span><span class="w">
|
||||
</span>
|
||||
<span class="w"> </span><span class="nt">environment</span><span class="p">:</span><span class="w">
|
||||
</span>
|
||||
<span class="w"></span><span class="w"> </span><span class="nt">GOPROXY</span><span class="p">:</span><span class="w"> </span><span class="l">https://goproxy.cn</span><span class="w">
|
||||
</span>
|
||||
<span class="w"> </span><span class="nt">commands</span><span class="p">:</span><span class="w">
|
||||
</span>
|
||||
<span class="w"></span><span class="w"> </span>- <span class="l">go get -u</span><span class="w">
|
||||
</span>
|
||||
<span class="w"> </span>- <span class="l">go build -v</span><span class="w">
|
||||
</span>
|
||||
<span class="w"> </span>- <span class="l">go test -v -race -coverprofile=coverage.txt -covermode=atomic</span>
|
||||
`),
|
||||
},
|
||||
{
|
||||
@@ -76,19 +87,30 @@ func TestFile(t *testing.T) {
|
||||
- go test -v -race -coverprofile=coverage.txt -covermode=atomic
|
||||
`)+"\n", "name: default", "name: default ", 1),
|
||||
want: util.Dedent(`
|
||||
<span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">pipeline</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">default </span>
|
||||
</span></span><span class="line"><span class="cl">
|
||||
</span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">steps</span><span class="p">:</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">test</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">golang:1.13</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">environment</span><span class="p">:</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"></span><span class="w"> </span><span class="nt">GOPROXY</span><span class="p">:</span><span class="w"> </span><span class="l">https://goproxy.cn</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">commands</span><span class="p">:</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"></span><span class="w"> </span>- <span class="l">go get -u</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">go build -v</span>
|
||||
</span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="l">go test -v -race -coverprofile=coverage.txt -covermode=atomic</span>
|
||||
</span></span>
|
||||
<span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">pipeline</span><span class="w">
|
||||
</span>
|
||||
<span class="w"></span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">default </span><span class="w">
|
||||
</span>
|
||||
<span class="w">
|
||||
</span>
|
||||
<span class="w"></span><span class="nt">steps</span><span class="p">:</span><span class="w">
|
||||
</span>
|
||||
<span class="w"></span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">test</span><span class="w">
|
||||
</span>
|
||||
<span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">golang:1.13</span><span class="w">
|
||||
</span>
|
||||
<span class="w"> </span><span class="nt">environment</span><span class="p">:</span><span class="w">
|
||||
</span>
|
||||
<span class="w"></span><span class="w"> </span><span class="nt">GOPROXY</span><span class="p">:</span><span class="w"> </span><span class="l">https://goproxy.cn</span><span class="w">
|
||||
</span>
|
||||
<span class="w"> </span><span class="nt">commands</span><span class="p">:</span><span class="w">
|
||||
</span>
|
||||
<span class="w"></span><span class="w"> </span>- <span class="l">go get -u</span><span class="w">
|
||||
</span>
|
||||
<span class="w"> </span>- <span class="l">go build -v</span><span class="w">
|
||||
</span>
|
||||
<span class="w"> </span>- <span class="l">go test -v -race -coverprofile=coverage.txt -covermode=atomic</span><span class="w">
|
||||
</span>
|
||||
<span class="w">
|
||||
</span>
|
||||
`),
|
||||
|
||||
@@ -78,6 +78,11 @@ func (hl *HostMatchList) AppendBuiltin(builtin string) {
|
||||
hl.builtins = append(hl.builtins, builtin)
|
||||
}
|
||||
|
||||
// AppendPattern appends more pattern to match
|
||||
func (hl *HostMatchList) AppendPattern(pattern string) {
|
||||
hl.patterns = append(hl.patterns, pattern)
|
||||
}
|
||||
|
||||
// IsEmpty checks if the checklist is empty
|
||||
func (hl *HostMatchList) IsEmpty() bool {
|
||||
return hl == nil || (len(hl.builtins) == 0 && len(hl.patterns) == 0 && len(hl.ipNets) == 0)
|
||||
|
||||
@@ -27,21 +27,21 @@ func NewContentStore() *ContentStore {
|
||||
|
||||
// Get gets a package blob
|
||||
func (s *ContentStore) Get(key BlobHash256Key) (storage.Object, error) {
|
||||
return s.store.Open(keyToRelativePath(key))
|
||||
return s.store.Open(KeyToRelativePath(key))
|
||||
}
|
||||
|
||||
// Save stores a package blob
|
||||
func (s *ContentStore) Save(key BlobHash256Key, r io.Reader, size int64) error {
|
||||
_, err := s.store.Save(keyToRelativePath(key), r, size)
|
||||
_, err := s.store.Save(KeyToRelativePath(key), r, size)
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete deletes a package blob
|
||||
func (s *ContentStore) Delete(key BlobHash256Key) error {
|
||||
return s.store.Delete(keyToRelativePath(key))
|
||||
return s.store.Delete(KeyToRelativePath(key))
|
||||
}
|
||||
|
||||
// keyToRelativePath converts the sha256 key aabb000000... to aa/bb/aabb000000...
|
||||
func keyToRelativePath(key BlobHash256Key) string {
|
||||
// KeyToRelativePath converts the sha256 key aabb000000... to aa/bb/aabb000000...
|
||||
func KeyToRelativePath(key BlobHash256Key) string {
|
||||
return path.Join(string(key)[0:2], string(key)[2:4], string(key))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package packages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHashedBuffer(t *testing.T) {
|
||||
cases := []struct {
|
||||
MaxMemorySize int
|
||||
Data string
|
||||
HashMD5 string
|
||||
HashSHA1 string
|
||||
HashSHA256 string
|
||||
HashSHA512 string
|
||||
}{
|
||||
{5, "test", "098f6bcd4621d373cade4e832627b4f6", "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff"},
|
||||
{5, "testtest", "05a671c66aefea124cc08b76ea6d30bb", "51abb9636078defbf888d8457a7c76f85c8f114c", "37268335dd6931045bdcdf92623ff819a64244b53d0e746d438797349d4da578", "125d6d03b32c84d492747f79cf0bf6e179d287f341384eb5d6d3197525ad6be8e6df0116032935698f99a09e265073d1d6c32c274591bf1d0a20ad67cba921bc"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
buf, err := CreateHashedBufferFromReader(strings.NewReader(c.Data), c.MaxMemorySize)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, len(c.Data), buf.Size())
|
||||
|
||||
data, err := io.ReadAll(buf)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, c.Data, string(data))
|
||||
|
||||
hashMD5, hashSHA1, hashSHA256, hashSHA512 := buf.Sums()
|
||||
assert.Equal(t, c.HashMD5, fmt.Sprintf("%x", hashMD5))
|
||||
assert.Equal(t, c.HashSHA1, fmt.Sprintf("%x", hashSHA1))
|
||||
assert.Equal(t, c.HashSHA256, fmt.Sprintf("%x", hashSHA256))
|
||||
assert.Equal(t, c.HashSHA512, fmt.Sprintf("%x", hashSHA512))
|
||||
|
||||
assert.NoError(t, buf.Close())
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -153,6 +154,10 @@ func createDelegateHooks(repoPath string) (err error) {
|
||||
}
|
||||
|
||||
func checkExecutable(filename string) bool {
|
||||
// windows has no concept of a executable bit
|
||||
if runtime.GOOS == "windows" {
|
||||
return true
|
||||
}
|
||||
fileInfo, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return false
|
||||
|
||||
@@ -48,6 +48,7 @@ var (
|
||||
DefaultBranch string
|
||||
AllowAdoptionOfUnadoptedRepositories bool
|
||||
AllowDeleteOfUnadoptedRepositories bool
|
||||
DisableDownloadSourceArchives bool
|
||||
|
||||
// Repository editor settings
|
||||
Editor struct {
|
||||
|
||||
@@ -75,11 +75,21 @@ func sessionHandler(session ssh.Session) {
|
||||
ctx, cancel := context.WithCancel(session.Context())
|
||||
defer cancel()
|
||||
|
||||
gitProtocol := ""
|
||||
for _, env := range session.Environ() {
|
||||
if strings.HasPrefix(env, "GIT_PROTOCOL=") {
|
||||
// The value would be version=2, so using normal split doesn't work here.
|
||||
gitProtocol = strings.SplitN(env, "=", 2)[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, setting.AppPath, args...)
|
||||
cmd.Env = append(
|
||||
os.Environ(),
|
||||
"SSH_ORIGINAL_COMMAND="+command,
|
||||
"SKIP_MINWINSVC=1",
|
||||
"GIT_PROTOCOL="+gitProtocol,
|
||||
)
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
|
||||
@@ -35,10 +35,11 @@ func BaseVars() Vars {
|
||||
"IsLandingPageExplore": setting.LandingPageURL == setting.LandingPageExplore,
|
||||
"IsLandingPageOrganizations": setting.LandingPageURL == setting.LandingPageOrganizations,
|
||||
|
||||
"ShowRegistrationButton": setting.Service.ShowRegistrationButton,
|
||||
"ShowMilestonesDashboardPage": setting.Service.ShowMilestonesDashboardPage,
|
||||
"ShowFooterBranding": setting.ShowFooterBranding,
|
||||
"ShowFooterVersion": setting.ShowFooterVersion,
|
||||
"ShowRegistrationButton": setting.Service.ShowRegistrationButton,
|
||||
"ShowMilestonesDashboardPage": setting.Service.ShowMilestonesDashboardPage,
|
||||
"ShowFooterBranding": setting.ShowFooterBranding,
|
||||
"ShowFooterVersion": setting.ShowFooterVersion,
|
||||
"DisableDownloadSourceArchives": setting.Repository.DisableDownloadSourceArchives,
|
||||
|
||||
"EnableSwagger": setting.API.EnableSwagger,
|
||||
"EnableOpenIDSignIn": setting.Service.EnableOpenIDSignIn,
|
||||
|
||||
@@ -103,35 +103,45 @@ func (b *FileBackedBuffer) Size() int64 {
|
||||
return b.size
|
||||
}
|
||||
|
||||
func (b *FileBackedBuffer) switchToReader() {
|
||||
func (b *FileBackedBuffer) switchToReader() error {
|
||||
if b.reader != nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if b.file != nil {
|
||||
if _, err := b.file.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
b.reader = b.file
|
||||
} else {
|
||||
b.reader = bytes.NewReader(b.buffer.Bytes())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read implements io.Reader
|
||||
func (b *FileBackedBuffer) Read(p []byte) (int, error) {
|
||||
b.switchToReader()
|
||||
if err := b.switchToReader(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return b.reader.Read(p)
|
||||
}
|
||||
|
||||
// ReadAt implements io.ReaderAt
|
||||
func (b *FileBackedBuffer) ReadAt(p []byte, off int64) (int, error) {
|
||||
b.switchToReader()
|
||||
if err := b.switchToReader(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return b.reader.ReadAt(p, off)
|
||||
}
|
||||
|
||||
// Seek implements io.Seeker
|
||||
func (b *FileBackedBuffer) Seek(offset int64, whence int) (int64, error) {
|
||||
b.switchToReader()
|
||||
if err := b.switchToReader(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return b.reader.Seek(offset, whence)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package filebuffer
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFileBackedBuffer(t *testing.T) {
|
||||
cases := []struct {
|
||||
MaxMemorySize int
|
||||
Data string
|
||||
}{
|
||||
{5, "test"},
|
||||
{5, "testtest"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
buf, err := CreateFromReader(strings.NewReader(c.Data), c.MaxMemorySize)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, len(c.Data), buf.Size())
|
||||
|
||||
data, err := io.ReadAll(buf)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, c.Data, string(data))
|
||||
|
||||
assert.NoError(t, buf.Close())
|
||||
}
|
||||
}
|
||||
@@ -16,12 +16,28 @@ import (
|
||||
// 1563418 -> 2 weeks 4 days
|
||||
// 3937125s -> 1 month 2 weeks
|
||||
// 45677465s -> 1 year 6 months
|
||||
//
|
||||
// Magic numbers:
|
||||
// 3600 = 60 * 60 (amount of seconds in a hour)
|
||||
// 86400 = 60 * 60 * 24 (amount of seconds in a day)
|
||||
func SecToTime(duration int64) string {
|
||||
formattedTime := ""
|
||||
years := duration / (3600 * 24 * 7 * 4 * 12)
|
||||
months := (duration / (3600 * 24 * 30)) % 12
|
||||
weeks := (duration / (3600 * 24 * 7)) % 4
|
||||
days := (duration / (3600 * 24)) % 7
|
||||
|
||||
// The following four variables are calculated by taking
|
||||
// into account the previously calculated variables, this avoids
|
||||
// pitfalls when using remainders. As that could lead to incorrect
|
||||
// results when the calculated number equals the quotient number.
|
||||
remainingDays := duration / (60 * 60 * 24)
|
||||
years := remainingDays / 365
|
||||
remainingDays -= years * 365
|
||||
months := remainingDays * 12 / 365
|
||||
remainingDays -= months * 365 / 12
|
||||
weeks := remainingDays / 7
|
||||
remainingDays -= weeks * 7
|
||||
days := remainingDays
|
||||
|
||||
// The following three variables are calculated without depending
|
||||
// on the previous calculated variables.
|
||||
hours := (duration / 3600) % 24
|
||||
minutes := (duration / 60) % 60
|
||||
seconds := duration % 60
|
||||
|
||||
@@ -11,10 +11,21 @@ import (
|
||||
)
|
||||
|
||||
func TestSecToTime(t *testing.T) {
|
||||
assert.Equal(t, SecToTime(66), "1 minute 6 seconds")
|
||||
assert.Equal(t, SecToTime(52410), "14 hours 33 minutes")
|
||||
assert.Equal(t, SecToTime(563418), "6 days 12 hours")
|
||||
assert.Equal(t, SecToTime(1563418), "2 weeks 4 days")
|
||||
assert.Equal(t, SecToTime(3937125), "1 month 2 weeks")
|
||||
assert.Equal(t, SecToTime(45677465), "1 year 5 months")
|
||||
second := int64(1)
|
||||
minute := 60 * second
|
||||
hour := 60 * minute
|
||||
day := 24 * hour
|
||||
year := 365 * day
|
||||
|
||||
assert.Equal(t, "1 minute 6 seconds", SecToTime(minute+6*second))
|
||||
assert.Equal(t, "1 hour", SecToTime(hour))
|
||||
assert.Equal(t, "1 hour", SecToTime(hour+second))
|
||||
assert.Equal(t, "14 hours 33 minutes", SecToTime(14*hour+33*minute+30*second))
|
||||
assert.Equal(t, "6 days 12 hours", SecToTime(6*day+12*hour+30*minute+18*second))
|
||||
assert.Equal(t, "2 weeks 4 days", SecToTime((2*7+4)*day+2*hour+16*minute+58*second))
|
||||
assert.Equal(t, "4 weeks", SecToTime(4*7*day))
|
||||
assert.Equal(t, "4 weeks 1 day", SecToTime((4*7+1)*day))
|
||||
assert.Equal(t, "1 month 2 weeks", SecToTime((6*7+3)*day+13*hour+38*minute+45*second))
|
||||
assert.Equal(t, "11 months", SecToTime(year-25*day))
|
||||
assert.Equal(t, "1 year 5 months", SecToTime(year+163*day+10*hour+11*minute+5*second))
|
||||
}
|
||||
|
||||
@@ -2886,6 +2886,7 @@ monitor.queue.nopool.title = No Worker Pool
|
||||
monitor.queue.nopool.desc = This queue wraps other queues and does not itself have a worker pool.
|
||||
monitor.queue.wrapped.desc = A wrapped queue wraps a slow starting queue, buffering queued requests in a channel. It does not have a worker pool itself.
|
||||
monitor.queue.persistable-channel.desc = A persistable-channel wraps two queues, a channel queue that has its own worker pool and a level queue for persisted requests from previous shutdowns. It does not have a worker pool itself.
|
||||
monitor.queue.flush = Flush worker
|
||||
monitor.queue.pool.timeout = Timeout
|
||||
monitor.queue.pool.addworkers.title = Add Workers
|
||||
monitor.queue.pool.addworkers.submit = Add Workers
|
||||
|
||||
@@ -1282,7 +1282,7 @@ issues.reopened_at=`переоткрыл(а) эту проблему <a id="%[1]
|
||||
issues.commit_ref_at=`упомянул эту задачу в коммите <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.ref_issue_from=`<a href="%[3]s">ссылка на эту проблему %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.ref_pull_from=`<a href="%[3]s">ссылается на этот запрос на слияние %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.ref_closing_from=`<a href="%[3]s">ссылается на запрос на слияние %[4], который закроет эту задачу</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.ref_closing_from=`<a href="%[3]s">ссылается на запрос на слияние %[4]s, который закроет эту задачу</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.ref_reopening_from=`<a href="%[3]s">ссылается на запрос на слияние %[4]s, который вновь откроет эту задачу</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.ref_closed_from=`<a href="%[3]s">закрыл этот запрос %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.ref_reopened_from=`<a href="%[3]s">переоткрыл эту задачу %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
|
||||
@@ -45,6 +45,7 @@ func Routes() *web.Route {
|
||||
authMethods := []auth.Method{
|
||||
&auth.OAuth2{},
|
||||
&auth.Basic{},
|
||||
&nuget.Auth{},
|
||||
&conan.Auth{},
|
||||
}
|
||||
if setting.Service.EnableReverseProxyAuth {
|
||||
|
||||
@@ -312,6 +312,9 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// keep download count on overwrite
|
||||
_pv.DownloadCount = pv.DownloadCount
|
||||
|
||||
if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil {
|
||||
log.Error("Error inserting package: %v", err)
|
||||
return nil, err
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package nuget
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
)
|
||||
|
||||
type Auth struct{}
|
||||
|
||||
func (a *Auth) Name() string {
|
||||
return "nuget"
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/nuget/api/package-publish-resource#request-parameters
|
||||
func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) *user_model.User {
|
||||
token, err := models.GetAccessTokenBySHA(req.Header.Get("X-NuGet-ApiKey"))
|
||||
if err != nil {
|
||||
if !(models.IsErrAccessTokenNotExist(err) || models.IsErrAccessTokenEmpty(err)) {
|
||||
log.Error("GetAccessTokenBySHA: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
u, err := user_model.GetUserByID(token.UID)
|
||||
if err != nil {
|
||||
log.Error("GetUserByID: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
token.UpdatedUnix = timeutil.TimeStampNow()
|
||||
if err := models.UpdateAccessToken(token); err != nil {
|
||||
log.Error("UpdateAccessToken: %v", err)
|
||||
}
|
||||
|
||||
return u
|
||||
}
|
||||
@@ -215,7 +215,7 @@ func UploadPackage(ctx *context.Context) {
|
||||
)
|
||||
if err != nil {
|
||||
if err == packages_model.ErrDuplicatePackageVersion {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
apiError(ctx, http.StatusConflict, err)
|
||||
return
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
@@ -272,7 +272,7 @@ func UploadSymbolPackage(ctx *context.Context) {
|
||||
case packages_model.ErrPackageNotExist:
|
||||
apiError(ctx, http.StatusNotFound, err)
|
||||
case packages_model.ErrDuplicatePackageFile:
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
apiError(ctx, http.StatusConflict, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
@@ -297,7 +297,7 @@ func UploadSymbolPackage(ctx *context.Context) {
|
||||
if err != nil {
|
||||
switch err {
|
||||
case packages_model.ErrDuplicatePackageFile:
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
apiError(ctx, http.StatusConflict, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
@@ -412,4 +412,6 @@ func DeletePackage(ctx *context.Context) {
|
||||
}
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -262,7 +262,7 @@ func EditTeam(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
if form.CanCreateOrgRepo != nil {
|
||||
team.CanCreateOrgRepo = *form.CanCreateOrgRepo
|
||||
team.CanCreateOrgRepo = team.IsOwnerTeam() || *form.CanCreateOrgRepo
|
||||
}
|
||||
|
||||
if len(form.Name) > 0 {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
@@ -59,7 +60,7 @@ func SendEmail(ctx *context.PrivateContext) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err := user_model.IterateUser(func(user *user_model.User) error {
|
||||
err := db.IterateObjects(ctx, func(user *user_model.User) error {
|
||||
if len(user.Email) > 0 && user.IsActive {
|
||||
emails = append(emails, user.Email)
|
||||
}
|
||||
|
||||
@@ -416,7 +416,11 @@ func EditTeamPost(ctx *context.Context) {
|
||||
isIncludeAllChanged = true
|
||||
t.IncludesAllRepositories = includesAllRepositories
|
||||
}
|
||||
t.CanCreateOrgRepo = form.CanCreateOrgRepo
|
||||
} else {
|
||||
t.CanCreateOrgRepo = true
|
||||
}
|
||||
|
||||
t.Description = form.Description
|
||||
if t.AccessMode < perm.AccessModeAdmin {
|
||||
units := make([]organization.TeamUnit, 0, len(unitPerms))
|
||||
@@ -433,7 +437,6 @@ func EditTeamPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.CanCreateOrgRepo = form.CanCreateOrgRepo
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplTeamNew)
|
||||
|
||||
@@ -474,11 +474,12 @@ func serviceRPC(ctx gocontext.Context, h serviceHandler, service string) {
|
||||
cmd := git.NewCommand(h.r.Context(), service, "--stateless-rpc", h.dir)
|
||||
cmd.SetDescription(fmt.Sprintf("%s %s %s [repo_path: %s]", git.GitExecutable, service, "--stateless-rpc", h.dir))
|
||||
if err := cmd.Run(&git.RunOpts{
|
||||
Dir: h.dir,
|
||||
Env: append(os.Environ(), h.environ...),
|
||||
Stdout: h.w,
|
||||
Stdin: reqBody,
|
||||
Stderr: &stderr,
|
||||
Dir: h.dir,
|
||||
Env: append(os.Environ(), h.environ...),
|
||||
Stdout: h.w,
|
||||
Stdin: reqBody,
|
||||
Stderr: &stderr,
|
||||
UseContextTimeout: true,
|
||||
}); err != nil {
|
||||
if err.Error() != "signal: killed" {
|
||||
log.Error("Fail to serve RPC(%s) in %s: %v - %s", service, h.dir, err, stderr.String())
|
||||
|
||||
@@ -510,6 +510,8 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C
|
||||
return nil
|
||||
}
|
||||
ctx.Data["GetCommitMessages"] = pull_service.GetSquashMergeCommitMessages(ctx, pull)
|
||||
} else {
|
||||
ctx.Data["GetCommitMessages"] = ""
|
||||
}
|
||||
|
||||
sha, err := baseGitRepo.GetRefCommitID(pull.GetGitRefName())
|
||||
|
||||
@@ -607,10 +607,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
||||
var shownIssues int
|
||||
if !isShowClosed {
|
||||
shownIssues = int(issueStats.OpenCount)
|
||||
ctx.Data["TotalIssueCount"] = shownIssues
|
||||
} else {
|
||||
shownIssues = int(issueStats.ClosedCount)
|
||||
ctx.Data["TotalIssueCount"] = shownIssues
|
||||
}
|
||||
if len(repoIDs) != 0 {
|
||||
shownIssues = 0
|
||||
@@ -619,6 +617,12 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
||||
}
|
||||
}
|
||||
|
||||
var allIssueCount int64
|
||||
for _, issueCount := range issueCountByRepo {
|
||||
allIssueCount += issueCount
|
||||
}
|
||||
ctx.Data["TotalIssueCount"] = allIssueCount
|
||||
|
||||
ctx.Data["IsShowClosed"] = isShowClosed
|
||||
|
||||
ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, ctx.FormString("RepoLink"))
|
||||
|
||||
+8
-1
@@ -289,6 +289,13 @@ func RegisterRoutes(m *web.Route) {
|
||||
}
|
||||
}
|
||||
|
||||
dlSourceEnabled := func(ctx *context.Context) {
|
||||
if setting.Repository.DisableDownloadSourceArchives {
|
||||
ctx.Error(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: not all routes need go through same middleware.
|
||||
// Especially some AJAX requests, we can reduce middleware number to improve performance.
|
||||
// Routers.
|
||||
@@ -1096,7 +1103,7 @@ func RegisterRoutes(m *web.Route) {
|
||||
m.Group("/archive", func() {
|
||||
m.Get("/*", repo.Download)
|
||||
m.Post("/*", repo.InitiateDownload)
|
||||
}, repo.MustBeNotEmpty, reqRepoCodeReader)
|
||||
}, repo.MustBeNotEmpty, dlSourceEnabled, reqRepoCodeReader)
|
||||
|
||||
m.Group("/branches", func() {
|
||||
m.Get("", repo.Branches)
|
||||
|
||||
@@ -474,5 +474,10 @@ func Init() error {
|
||||
allowList.AppendBuiltin(hostmatcher.MatchBuiltinPrivate)
|
||||
allowList.AppendBuiltin(hostmatcher.MatchBuiltinLoopback)
|
||||
}
|
||||
|
||||
if setting.Proxy.Enabled && setting.Proxy.ProxyURLFixed != nil {
|
||||
allowList.AppendPattern(setting.Proxy.ProxyURLFixed.Host)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -448,7 +448,7 @@ func GetFileStreamByPackageVersionAndFileID(ctx context.Context, owner *user_mod
|
||||
|
||||
// GetFileStreamByPackageVersion returns the content of the specific package file
|
||||
func GetFileStreamByPackageVersion(ctx context.Context, pv *packages_model.PackageVersion, pfi *PackageFileInfo) (io.ReadCloser, *packages_model.PackageFile, error) {
|
||||
pf, err := packages_model.GetFileForVersionByName(db.DefaultContext, pv.ID, pfi.Filename, pfi.CompositeKey)
|
||||
pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, pfi.Filename, pfi.CompositeKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -87,6 +87,9 @@ func IsUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest,
|
||||
}
|
||||
headRepoPerm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, user)
|
||||
if err != nil {
|
||||
if repo_model.IsErrUnitTypeNotExist(err) {
|
||||
return false, false, nil
|
||||
}
|
||||
return false, false, err
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ func DeleteAvatar(repo *repo_model.Repository) error {
|
||||
|
||||
// RemoveRandomAvatars removes the randomly generated avatars that were created for repositories
|
||||
func RemoveRandomAvatars(ctx context.Context) error {
|
||||
return repo_model.IterateRepository(func(repository *repo_model.Repository) error {
|
||||
return db.IterateObjects(ctx, func(repository *repo_model.Repository) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return db.ErrCancelledf("before random avatars removed for %s", repository.FullName())
|
||||
|
||||
@@ -7,12 +7,10 @@ package repository
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/notification"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
)
|
||||
@@ -23,6 +21,11 @@ func GenerateIssueLabels(ctx context.Context, templateRepo, generateRepo *repo_m
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Prevent insert being called with an empty slice which would result in
|
||||
// err "no element on slice when insert".
|
||||
if len(templateLabels) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
newLabels := make([]*issues_model.Label, 0, len(templateLabels))
|
||||
for _, templateLabel := range templateLabels {
|
||||
@@ -95,11 +98,6 @@ func GenerateRepository(doer, owner *user_model.User, templateRepo *repo_model.R
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
if generateRepo != nil && generateRepo.ID > 0 {
|
||||
if errDelete := models.DeleteRepository(doer, owner.ID, generateRepo.ID); errDelete != nil {
|
||||
log.Error("Rollback deleteRepository: %v", errDelete)
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -31,12 +31,14 @@
|
||||
<br>
|
||||
{{.i18n.Tr "mail.release.downloads"}}
|
||||
<ul>
|
||||
{{if not .DisableDownloadSourceArchives}}
|
||||
<li>
|
||||
<a href="{{.Release.Repo.Link}}/archive/{{.Release.TagName | PathEscapeSegments}}.zip" rel="nofollow"><strong>{{.i18n.Tr "mail.release.download.zip"}}</strong></a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{.Release.Repo.Link}}/archive/{{.Release.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow"><strong>{{.i18n.Tr "mail.release.download.targz"}}</strong></a>
|
||||
</li>
|
||||
{{end}}
|
||||
{{if .Release.Attachments}}
|
||||
{{range .Release.Attachments}}
|
||||
<li>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{{if .PackageDescriptor.Metadata.Author}}<div class="item" title="{{.i18n.Tr "packages.details.author"}}">{{svg "octicon-person" 16 "mr-3"}} {{.PackageDescriptor.Metadata.Author}}</div>{{end}}
|
||||
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{.i18n.Tr "packages.details.project_site"}}</a></div>{{end}}
|
||||
{{if .PackageDescriptor.Metadata.License}}<div class="item" title="{{.i18n.Tr "packages.details.license"}}">{{svg "octicon-law" 16 "mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
|
||||
{{range .PackageDescriptor.Properties}}
|
||||
{{range .PackageDescriptor.VersionProperties}}
|
||||
{{if eq .Name "npm.tag"}}<div class="item" title="{{$.i18n.Tr "packages.npm.details.tag"}}">{{svg "octicon-versions" 16 "mr-3"}} {{.Value}}</div>{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -26,13 +26,15 @@
|
||||
{{svg "octicon-git-branch"}}
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="ui basic jump dropdown icon button tooltip" data-content="{{$.i18n.Tr "repo.branch.download" ($.DefaultBranch)}}" data-position="top right">
|
||||
{{if not $.DisableDownloadSourceArchives}}
|
||||
<div class="ui basic jump dropdown icon button tooltip" data-content="{{$.i18n.Tr "repo.branch.download" ($.DefaultBranch)}}" data-position="top right">
|
||||
{{svg "octicon-download"}}
|
||||
<div class="menu">
|
||||
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.DefaultBranch}}.zip" rel="nofollow">{{svg "octicon-file-zip"}} ZIP</a>
|
||||
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.DefaultBranch}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip"}} TAR.GZ</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -110,7 +112,7 @@
|
||||
{{svg "octicon-git-branch"}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if (not .IsDeleted)}}
|
||||
{{if and (not .IsDeleted) (not $.DisableDownloadSourceArchives)}}
|
||||
<div class="ui basic jump dropdown icon button tooltip" data-content="{{$.i18n.Tr "repo.branch.download" (.Name)}}" data-position="top right">
|
||||
{{svg "octicon-download"}}
|
||||
<div class="menu">
|
||||
|
||||
@@ -9,16 +9,7 @@
|
||||
SSH
|
||||
</button>
|
||||
{{end}}
|
||||
<!-- the value will be updated by initRepoCloneLink, the code below is used to avoid UI flicking -->
|
||||
<input id="repo-clone-url" value="" size="1" readonly>
|
||||
<script>
|
||||
(() => {
|
||||
const proto = localStorage.getItem('repo-clone-protocol') || 'https';
|
||||
const btn = document.getElementById(`repo-clone-${proto}`);
|
||||
// it's ok if we don't find the btn here, initRepoCloneLink will take care of it
|
||||
document.getElementById('repo-clone-url').value = btn ? btn.getAttribute('data-link') : '';
|
||||
})();
|
||||
</script>
|
||||
<input id="repo-clone-url" class="js-clone-url" value="{{$.CloneButtonOriginLink.HTTPS}}" size="1" readonly>
|
||||
<button class="ui basic icon button tooltip" id="clipboard-btn" data-content="{{.i18n.Tr "copy_url"}}" data-clipboard-target="#repo-clone-url" aria-label="{{.i18n.Tr "copy_url"}}">
|
||||
{{svg "octicon-paste"}}
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<script>
|
||||
// synchronously set clone button states and urls here to avoid flickering
|
||||
// on page load. initRepoCloneLink calls this when proto changes.
|
||||
// TODO: This localStorage setting should be moved to backend user config
|
||||
// so it's available during rendering, then this inline script can be removed.
|
||||
(window.updateCloneStates = function() {
|
||||
const httpsBtn = document.getElementById('repo-clone-https');
|
||||
const sshBtn = document.getElementById('repo-clone-ssh');
|
||||
const value = localStorage.getItem('repo-clone-protocol') || 'https';
|
||||
const isSSH = value === 'ssh' && sshBtn || value !== 'ssh' && !httpsBtn;
|
||||
|
||||
if (httpsBtn) httpsBtn.classList[!isSSH ? 'add' : 'remove']('primary');
|
||||
if (sshBtn) sshBtn.classList[isSSH ? 'add' : 'remove']('primary');
|
||||
|
||||
const btn = isSSH ? sshBtn : httpsBtn;
|
||||
if (!btn) return;
|
||||
|
||||
const link = btn.getAttribute('data-link');
|
||||
for (const el of document.getElementsByClassName('js-clone-url')) {
|
||||
el[el.nodeName === 'INPUT' ? 'value' : 'textContent'] = link;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@@ -37,7 +37,7 @@ git init
|
||||
{{if ne .Repository.DefaultBranch "master"}}git checkout -b {{.Repository.DefaultBranch}}{{end}}
|
||||
git add README.md
|
||||
git commit -m "first commit"
|
||||
git remote add origin <span class="clone-url"></span>
|
||||
git remote add origin <span class="js-clone-url">{{$.CloneButtonOriginLink.HTTPS}}</span>
|
||||
git push -u origin {{.Repository.DefaultBranch}}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,24 +46,11 @@ git push -u origin {{.Repository.DefaultBranch}}</code></pre>
|
||||
<div class="item">
|
||||
<h3>{{.i18n.Tr "repo.push_exist_repo"}}</h3>
|
||||
<div class="markup">
|
||||
<pre><code>git remote add origin <span class="clone-url"></span>
|
||||
<pre><code>git remote add origin <span class="js-clone-url">{{$.CloneButtonOriginLink.HTTPS}}</span>
|
||||
git push -u origin {{.Repository.DefaultBranch}}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<!-- the clone-url content will be updated by initRepoCloneLink, the code below is used to avoid UI flicking -->
|
||||
<script>
|
||||
(() => {
|
||||
const proto = localStorage.getItem('repo-clone-protocol') || 'https';
|
||||
const btn = document.getElementById(`repo-clone-${proto}`);
|
||||
const cloneUrls = document.getElementsByClassName('clone-url');
|
||||
// it's ok if we didn't find the btn here, initRepoCloneLink will take all the work
|
||||
if (btn) {
|
||||
for (let i = 0; i < cloneUrls.length; i++) {
|
||||
cloneUrls[i].textContent = btn.getAttribute('data-link');
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{{template "repo/clone_script" .}}
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="ui segment center">
|
||||
|
||||
@@ -124,12 +124,15 @@
|
||||
{{if eq $n 0}}
|
||||
<div class="ui action tiny input" id="clone-panel">
|
||||
{{template "repo/clone_buttons" .}}
|
||||
{{template "repo/clone_script" .}}
|
||||
<button id="download-btn" class="ui basic jump dropdown icon button tooltip" data-content="{{.i18n.Tr "repo.download_archive"}}" data-position="top right">
|
||||
{{svg "octicon-download"}}
|
||||
<div class="menu">
|
||||
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.BranchName}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "mr-3"}}{{.i18n.Tr "repo.download_zip"}}</a>
|
||||
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.BranchName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "mr-3"}}{{.i18n.Tr "repo.download_tar"}}</a>
|
||||
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.BranchName}}.bundle" rel="nofollow">{{svg "octicon-package" 16 "mr-3"}}{{.i18n.Tr "repo.download_bundle"}}</a>
|
||||
{{if not $.DisableDownloadSourceArchives}}
|
||||
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.BranchName}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "mr-3"}}{{.i18n.Tr "repo.download_zip"}}</a>
|
||||
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.BranchName}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "mr-3"}}{{.i18n.Tr "repo.download_tar"}}</a>
|
||||
<a class="item archive-link" href="{{$.RepoLink}}/archive/{{PathEscapeSegments $.BranchName}}.bundle" rel="nofollow">{{svg "octicon-package" 16 "mr-3"}}{{.i18n.Tr "repo.download_bundle"}}</a>
|
||||
{{end}}
|
||||
<a class="item" href="vscode://vscode.git/clone?url={{$.RepoCloneLink.HTTPS}}">{{svg "gitea-vscode" 16 "mr-3"}}{{.i18n.Tr "repo.clone_in_vsc"}}</a>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -358,6 +358,7 @@
|
||||
'emptyCommit': {{.Issue.PullRequest.IsEmpty}},
|
||||
'pullHeadCommitID': {{.PullHeadCommitID}},
|
||||
'isPullBranchDeletable': {{.IsPullBranchDeletable}},
|
||||
'defaultMergeStyle': {{.MergeStyle}},
|
||||
'defaultDeleteBranchAfterMerge': {{$prUnit.PullRequestsConfig.DefaultDeleteBranchAfterMerge}},
|
||||
'mergeMessageFieldPlaceHolder': {{$.i18n.Tr "repo.editor.commit_message_desc"}},
|
||||
|
||||
@@ -395,7 +396,7 @@
|
||||
'allowed': {{$prUnit.PullRequestsConfig.AllowSquash}},
|
||||
'textDoMerge': {{$.i18n.Tr "repo.pulls.squash_merge_pull_request"}},
|
||||
'mergeTitleFieldText': defaultSquashMergeTitle,
|
||||
'mergeMessageFieldText': defaultMergeMessage,
|
||||
'mergeMessageFieldText': {{.GetCommitMessages}} + defaultMergeMessage,
|
||||
'hideAutoMerge': generalHideAutoMerge,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -37,8 +37,10 @@
|
||||
<div class="download df ac">
|
||||
{{if $.Permission.CanRead $.UnitTypeCode}}
|
||||
<a class="mr-3 mono" href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "mr-2"}}{{ShortSha .Sha1}}</a>
|
||||
<a class="archive-link mr-3" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "mr-2"}}ZIP</a>
|
||||
<a class="archive-link mr-3" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "mr-2"}}TAR.GZ</a>
|
||||
{{if not $.DisableDownloadSourceArchives}}
|
||||
<a class="archive-link mr-3" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "mr-2"}}ZIP</a>
|
||||
<a class="archive-link mr-3" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "mr-2"}}TAR.GZ</a>
|
||||
{{end}}
|
||||
{{if (and $.CanCreateRelease $release.IsTag)}}
|
||||
<a class="mr-3" href="{{$.RepoLink}}/releases/new?tag={{.TagName}}">{{svg "octicon-tag" 16 "mr-2"}}{{$.i18n.Tr "repo.release.new_release"}}</a>
|
||||
{{end}}
|
||||
@@ -104,8 +106,10 @@
|
||||
<div class="download">
|
||||
{{if $.Permission.CanRead $.UnitTypeCode}}
|
||||
<a class="mono" href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "mr-2"}}{{ShortSha .Sha1}}</a>
|
||||
<a class="archive-link" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.zip" rel="nofollow">{{svg "octicon-file-zip"}} ZIP</a>
|
||||
<a class="archive-link" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip"}} TAR.GZ</a>
|
||||
{{if not $.DisableDownloadSourceArchives}}
|
||||
<a class="archive-link" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.zip" rel="nofollow">{{svg "octicon-file-zip"}} ZIP</a>
|
||||
<a class="archive-link" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip"}} TAR.GZ</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
@@ -146,7 +150,7 @@
|
||||
{{$.i18n.Tr "repo.release.downloads"}}
|
||||
</summary>
|
||||
<ul class="list">
|
||||
{{if and (not .IsDraft) ($.Permission.CanRead $.UnitTypeCode)}}
|
||||
{{if and (not $.DisableDownloadSourceArchives) (not .IsDraft) ($.Permission.CanRead $.UnitTypeCode)}}
|
||||
<li>
|
||||
<a class="archive-link" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.zip" rel="nofollow"><strong>{{svg "octicon-file-zip" 16 "mr-2"}}{{$.i18n.Tr "repo.release.source_code"}} (ZIP)</strong></a>
|
||||
</li>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<div class="ui eight wide column text right df ac je">
|
||||
<div class="ui action small input" id="clone-panel">
|
||||
{{template "repo/clone_buttons" .}}
|
||||
{{template "repo/clone_script" .}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui header eight wide column">
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
<div class="right fitted item">
|
||||
<div class="ui action small input" id="clone-panel">
|
||||
{{template "repo/clone_buttons" .}}
|
||||
{{template "repo/clone_script" .}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -60,19 +60,17 @@
|
||||
{{$.i18n.Tr .GetLastEventLabelFake $timeStr (.Poster.GetDisplayName | Escape) | Safe}}
|
||||
{{end}}
|
||||
{{if .IsPull}}
|
||||
<div class="branches">
|
||||
<div class="branches df ac">
|
||||
<div class="branch">
|
||||
<a class="bold" href="{{.PullRequest.BaseRepo.HTMLURL}}/src/branch/{{PathEscapeSegments .PullRequest.BaseBranch}}">
|
||||
<a href="{{.PullRequest.BaseRepo.HTMLURL}}/src/branch/{{PathEscapeSegments .PullRequest.BaseBranch}}">
|
||||
{{/* inline to remove the spaces between spans */}}
|
||||
{{if ne .RepoID .PullRequest.BaseRepoID}}<span class="truncated-name">{{.PullRequest.BaseRepo.OwnerName}}</span>:{{end}}<span class="truncated-name">{{.PullRequest.BaseBranch}}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
«
|
||||
|
||||
{{svg "gitea-double-chevron-left" 12 "mx-1"}}
|
||||
{{if .PullRequest.HeadRepo}}
|
||||
<div class="branch">
|
||||
<a class="bold" href="{{.PullRequest.HeadRepo.HTMLURL}}/src/branch/{{PathEscapeSegments .PullRequest.HeadBranch}}">
|
||||
<a href="{{.PullRequest.HeadRepo.HTMLURL}}/src/branch/{{PathEscapeSegments .PullRequest.HeadBranch}}">
|
||||
{{/* inline to remove the spaces between spans */}}
|
||||
{{if ne .RepoID .PullRequest.HeadRepoID}}<span class="truncated-name">{{.PullRequest.HeadRepo.OwnerName}}</span>:{{end}}<span class="truncated-name">{{.PullRequest.HeadBranch}}</span>
|
||||
</a>
|
||||
@@ -85,6 +83,11 @@
|
||||
{{svg "octicon-milestone" 14 "mr-2"}}{{.Milestone.Name}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if .Project}}
|
||||
<a class="project" {{if $.RepoLink}}href="{{$.RepoLink}}/projects/{{.Project.ID}}"{{else}}href="{{.Repo.Link}}/projects/{{.Project.ID}}"{{end}}>
|
||||
{{svg "octicon-project" 14 "mr-2"}}{{.Project.Title}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if .Ref}}
|
||||
<a class="ref" {{if $.RepoLink}}href="{{index $.IssueRefURLs .ID}}"{{else}}href="{{.Repo.Link}}{{index $.IssueRefURLs .ID}}"{{end}}>
|
||||
{{svg "octicon-git-branch" 14 "mr-2"}}{{index $.IssueRefEndNames .ID}}
|
||||
|
||||
@@ -145,7 +145,10 @@ export default {
|
||||
|
||||
created() {
|
||||
this.mergeStyleAllowedCount = this.mergeForm.mergeStyles.reduce((v, msd) => v + (msd.allowed ? 1 : 0), 0);
|
||||
this.switchMergeStyle(this.mergeForm.mergeStyles.find((e) => e.allowed)?.name, !this.mergeForm.canMergeNow);
|
||||
|
||||
let mergeStyle = this.mergeForm.mergeStyles.find((e) => e.allowed && e.name === this.mergeForm.defaultMergeStyle)?.name;
|
||||
if (!mergeStyle) mergeStyle = this.mergeForm.mergeStyles.find((e) => e.allowed)?.name;
|
||||
this.switchMergeStyle(mergeStyle, !this.mergeForm.canMergeNow);
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
||||
@@ -164,16 +164,12 @@ export function initGlobalCommon() {
|
||||
}
|
||||
});
|
||||
|
||||
// loading-button this logic used to prevent push one form more than one time
|
||||
$(document).on('click', '.button.loading-button', function (e) {
|
||||
const $btn = $(this);
|
||||
|
||||
if ($btn.hasClass('loading')) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
$btn.addClass('loading disabled');
|
||||
// prevent multiple form submissions on forms containing .loading-button
|
||||
document.addEventListener('submit', (e) => {
|
||||
const btn = e.target.querySelector('.loading-button');
|
||||
if (!btn) return;
|
||||
if (btn.classList.contains('loading')) return e.preventDefault();
|
||||
btn.classList.add('loading');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,13 @@ class Source {
|
||||
self.addEventListener('connect', (e) => {
|
||||
for (const port of e.ports) {
|
||||
port.addEventListener('message', (event) => {
|
||||
if (!self.EventSource) {
|
||||
// some browsers (like PaleMoon, Firefox<53) don't support EventSource in SharedWorkerGlobalScope.
|
||||
// this event handler needs EventSource when doing "new Source(url)", so just post a message back to the caller,
|
||||
// in case the caller would like to use a fallback method to do its work.
|
||||
port.postMessage({type: 'no-event-source'});
|
||||
return;
|
||||
}
|
||||
if (event.data.type === 'start') {
|
||||
const url = event.data.url;
|
||||
if (sourcesByUrl[url]) {
|
||||
|
||||
@@ -47,14 +47,24 @@ export function initNotificationCount() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (notificationSettings.EventSourceUpdateTime > 0 && !!window.EventSource && window.SharedWorker) {
|
||||
let usingPeriodicPoller = false;
|
||||
const startPeriodicPoller = (timeout, lastCount) => {
|
||||
if (timeout <= 0 || !Number.isFinite(timeout)) return;
|
||||
usingPeriodicPoller = true;
|
||||
lastCount = lastCount ?? notificationCount.text();
|
||||
setTimeout(async () => {
|
||||
await updateNotificationCountWithCallback(startPeriodicPoller, timeout, lastCount);
|
||||
}, timeout);
|
||||
};
|
||||
|
||||
if (notificationSettings.EventSourceUpdateTime > 0 && window.EventSource && window.SharedWorker) {
|
||||
// Try to connect to the event source via the shared worker first
|
||||
const worker = new SharedWorker(`${__webpack_public_path__}js/eventsource.sharedworker.js`, 'notification-worker');
|
||||
worker.addEventListener('error', (event) => {
|
||||
console.error(event);
|
||||
console.error('worker error', event);
|
||||
});
|
||||
worker.port.addEventListener('messageerror', () => {
|
||||
console.error('Unable to deserialize message');
|
||||
console.error('unable to deserialize message');
|
||||
});
|
||||
worker.port.postMessage({
|
||||
type: 'start',
|
||||
@@ -62,13 +72,16 @@ export function initNotificationCount() {
|
||||
});
|
||||
worker.port.addEventListener('message', (event) => {
|
||||
if (!event.data || !event.data.type) {
|
||||
console.error(event);
|
||||
console.error('unknown worker message event', event);
|
||||
return;
|
||||
}
|
||||
if (event.data.type === 'notification-count') {
|
||||
const _promise = receiveUpdateCount(event.data);
|
||||
} else if (event.data.type === 'no-event-source') {
|
||||
// browser doesn't support EventSource, falling back to periodic poller
|
||||
if (!usingPeriodicPoller) startPeriodicPoller(notificationSettings.MinTimeout);
|
||||
} else if (event.data.type === 'error') {
|
||||
console.error(event.data);
|
||||
console.error('worker port event error', event.data);
|
||||
} else if (event.data.type === 'logout') {
|
||||
if (event.data.data !== 'here') {
|
||||
return;
|
||||
@@ -86,7 +99,7 @@ export function initNotificationCount() {
|
||||
}
|
||||
});
|
||||
worker.port.addEventListener('error', (e) => {
|
||||
console.error(e);
|
||||
console.error('worker port error', e);
|
||||
});
|
||||
worker.port.start();
|
||||
window.addEventListener('beforeunload', () => {
|
||||
@@ -99,17 +112,7 @@ export function initNotificationCount() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (notificationSettings.MinTimeout <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fn = (timeout, lastCount) => {
|
||||
setTimeout(() => {
|
||||
const _promise = updateNotificationCountWithCallback(fn, timeout, lastCount);
|
||||
}, timeout);
|
||||
};
|
||||
|
||||
fn(notificationSettings.MinTimeout, notificationCount.text());
|
||||
startPeriodicPoller(notificationSettings.MinTimeout);
|
||||
}
|
||||
|
||||
async function updateNotificationCountWithCallback(callback, timeout, lastCount) {
|
||||
|
||||
@@ -44,8 +44,6 @@ export function initRepoArchiveLinks() {
|
||||
}
|
||||
|
||||
export function initRepoCloneLink() {
|
||||
const defaultGitProtocol = 'https'; // ssh or https
|
||||
|
||||
const $repoCloneSsh = $('#repo-clone-ssh');
|
||||
const $repoCloneHttps = $('#repo-clone-https');
|
||||
const $inputLink = $('#repo-clone-url');
|
||||
@@ -54,41 +52,19 @@ export function initRepoCloneLink() {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateUi = () => {
|
||||
let isSSH = (localStorage.getItem('repo-clone-protocol') || defaultGitProtocol) === 'ssh';
|
||||
// there must be at least one clone button (by context/repo.go). if no ssh, then there must be https.
|
||||
if (isSSH && $repoCloneSsh.length === 0) {
|
||||
isSSH = false;
|
||||
} else if (!isSSH && $repoCloneHttps.length === 0) {
|
||||
isSSH = true;
|
||||
}
|
||||
const cloneLink = (isSSH ? $repoCloneSsh : $repoCloneHttps).attr('data-link');
|
||||
$inputLink.val(cloneLink);
|
||||
if (isSSH) {
|
||||
$repoCloneSsh.addClass('primary');
|
||||
$repoCloneHttps.removeClass('primary');
|
||||
} else {
|
||||
$repoCloneSsh.removeClass('primary');
|
||||
$repoCloneHttps.addClass('primary');
|
||||
}
|
||||
// the empty repo guide
|
||||
$('.quickstart .empty-repo-guide .clone-url').text(cloneLink);
|
||||
};
|
||||
updateUi();
|
||||
|
||||
// restore animation after first init
|
||||
setTimeout(() => {
|
||||
// restore animation after first init
|
||||
$repoCloneSsh.removeClass('no-transition');
|
||||
$repoCloneHttps.removeClass('no-transition');
|
||||
}, 100);
|
||||
|
||||
$repoCloneSsh.on('click', () => {
|
||||
localStorage.setItem('repo-clone-protocol', 'ssh');
|
||||
updateUi();
|
||||
window.updateCloneStates();
|
||||
});
|
||||
$repoCloneHttps.on('click', () => {
|
||||
localStorage.setItem('repo-clone-protocol', 'https');
|
||||
updateUi();
|
||||
window.updateCloneStates();
|
||||
});
|
||||
|
||||
$inputLink.on('click', () => {
|
||||
|
||||
@@ -2,7 +2,6 @@ import $ from 'jquery';
|
||||
import prettyMilliseconds from 'pretty-ms';
|
||||
|
||||
const {appSubUrl, csrfToken, notificationSettings, enableTimeTracking} = window.config;
|
||||
let updateTimeInterval = null; // holds setInterval id when active
|
||||
|
||||
export function initStopwatch() {
|
||||
if (!enableTimeTracking) {
|
||||
@@ -26,14 +25,28 @@ export function initStopwatch() {
|
||||
$(this).parent().trigger('submit');
|
||||
});
|
||||
|
||||
if (notificationSettings.EventSourceUpdateTime > 0 && !!window.EventSource && window.SharedWorker) {
|
||||
// global stop watch (in the head_navbar), it should always work in any case either the EventSource or the PeriodicPoller is used.
|
||||
const currSeconds = $('.stopwatch-time').attr('data-seconds');
|
||||
if (currSeconds) {
|
||||
updateStopwatchTime(currSeconds);
|
||||
}
|
||||
|
||||
let usingPeriodicPoller = false;
|
||||
const startPeriodicPoller = (timeout) => {
|
||||
if (timeout <= 0 || !Number.isFinite(timeout)) return;
|
||||
usingPeriodicPoller = true;
|
||||
setTimeout(() => updateStopwatchWithCallback(startPeriodicPoller, timeout), timeout);
|
||||
};
|
||||
|
||||
// if the browser supports EventSource and SharedWorker, use it instead of the periodic poller
|
||||
if (notificationSettings.EventSourceUpdateTime > 0 && window.EventSource && window.SharedWorker) {
|
||||
// Try to connect to the event source via the shared worker first
|
||||
const worker = new SharedWorker(`${__webpack_public_path__}js/eventsource.sharedworker.js`, 'notification-worker');
|
||||
worker.addEventListener('error', (event) => {
|
||||
console.error(event);
|
||||
console.error('worker error', event);
|
||||
});
|
||||
worker.port.addEventListener('messageerror', () => {
|
||||
console.error('Unable to deserialize message');
|
||||
console.error('unable to deserialize message');
|
||||
});
|
||||
worker.port.postMessage({
|
||||
type: 'start',
|
||||
@@ -41,13 +54,16 @@ export function initStopwatch() {
|
||||
});
|
||||
worker.port.addEventListener('message', (event) => {
|
||||
if (!event.data || !event.data.type) {
|
||||
console.error(event);
|
||||
console.error('unknown worker message event', event);
|
||||
return;
|
||||
}
|
||||
if (event.data.type === 'stopwatches') {
|
||||
updateStopwatchData(JSON.parse(event.data.data));
|
||||
} else if (event.data.type === 'no-event-source') {
|
||||
// browser doesn't support EventSource, falling back to periodic poller
|
||||
if (!usingPeriodicPoller) startPeriodicPoller(notificationSettings.MinTimeout);
|
||||
} else if (event.data.type === 'error') {
|
||||
console.error(event.data);
|
||||
console.error('worker port event error', event.data);
|
||||
} else if (event.data.type === 'logout') {
|
||||
if (event.data.data !== 'here') {
|
||||
return;
|
||||
@@ -65,7 +81,7 @@ export function initStopwatch() {
|
||||
}
|
||||
});
|
||||
worker.port.addEventListener('error', (e) => {
|
||||
console.error(e);
|
||||
console.error('worker port error', e);
|
||||
});
|
||||
worker.port.start();
|
||||
window.addEventListener('beforeunload', () => {
|
||||
@@ -78,22 +94,7 @@ export function initStopwatch() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (notificationSettings.MinTimeout <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fn = (timeout) => {
|
||||
setTimeout(() => {
|
||||
const _promise = updateStopwatchWithCallback(fn, timeout);
|
||||
}, timeout);
|
||||
};
|
||||
|
||||
fn(notificationSettings.MinTimeout);
|
||||
|
||||
const currSeconds = $('.stopwatch-time').data('seconds');
|
||||
if (currSeconds) {
|
||||
updateTimeInterval = updateStopwatchTime(currSeconds);
|
||||
}
|
||||
startPeriodicPoller(notificationSettings.MinTimeout);
|
||||
}
|
||||
|
||||
async function updateStopwatchWithCallback(callback, timeout) {
|
||||
@@ -114,12 +115,6 @@ async function updateStopwatch() {
|
||||
url: `${appSubUrl}/user/stopwatches`,
|
||||
headers: {'X-Csrf-Token': csrfToken},
|
||||
});
|
||||
|
||||
if (updateTimeInterval) {
|
||||
clearInterval(updateTimeInterval);
|
||||
updateTimeInterval = null;
|
||||
}
|
||||
|
||||
return updateStopwatchData(data);
|
||||
}
|
||||
|
||||
@@ -127,10 +122,7 @@ function updateStopwatchData(data) {
|
||||
const watch = data[0];
|
||||
const btnEl = $('.active-stopwatch-trigger');
|
||||
if (!watch) {
|
||||
if (updateTimeInterval) {
|
||||
clearInterval(updateTimeInterval);
|
||||
updateTimeInterval = null;
|
||||
}
|
||||
clearStopwatchTimer();
|
||||
btnEl.addClass('hidden');
|
||||
} else {
|
||||
const {repo_owner_name, repo_name, issue_index, seconds} = watch;
|
||||
@@ -139,22 +131,31 @@ function updateStopwatchData(data) {
|
||||
$('.stopwatch-commit').attr('action', `${issueUrl}/times/stopwatch/toggle`);
|
||||
$('.stopwatch-cancel').attr('action', `${issueUrl}/times/stopwatch/cancel`);
|
||||
$('.stopwatch-issue').text(`${repo_owner_name}/${repo_name}#${issue_index}`);
|
||||
$('.stopwatch-time').text(prettyMilliseconds(seconds * 1000));
|
||||
updateStopwatchTime(seconds);
|
||||
btnEl.removeClass('hidden');
|
||||
}
|
||||
|
||||
return !!data.length;
|
||||
}
|
||||
|
||||
let updateTimeIntervalId = null; // holds setInterval id when active
|
||||
function clearStopwatchTimer() {
|
||||
if (updateTimeIntervalId !== null) {
|
||||
clearInterval(updateTimeIntervalId);
|
||||
updateTimeIntervalId = null;
|
||||
}
|
||||
}
|
||||
function updateStopwatchTime(seconds) {
|
||||
const secs = parseInt(seconds);
|
||||
if (!Number.isFinite(secs)) return;
|
||||
|
||||
clearStopwatchTimer();
|
||||
const $stopwatch = $('.stopwatch-time');
|
||||
const start = Date.now();
|
||||
updateTimeInterval = setInterval(() => {
|
||||
const updateUi = () => {
|
||||
const delta = Date.now() - start;
|
||||
const dur = prettyMilliseconds(secs * 1000 + delta, {compact: true});
|
||||
$('.stopwatch-time').text(dur);
|
||||
}, 1000);
|
||||
$stopwatch.text(dur);
|
||||
};
|
||||
updateUi();
|
||||
updateTimeIntervalId = setInterval(updateUi, 1000);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {emojiKeys, emojiHTML, emojiString} from './emoji.js';
|
||||
import {uniq} from '../utils.js';
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
|
||||
function makeCollections({mentions, emoji}) {
|
||||
const collections = [];
|
||||
@@ -24,7 +25,7 @@ function makeCollections({mentions, emoji}) {
|
||||
return emojiString(item.original);
|
||||
},
|
||||
menuItemTemplate: (item) => {
|
||||
return `<div class="tribute-item">${emojiHTML(item.original)}<span>${item.original}</span></div>`;
|
||||
return `<div class="tribute-item">${emojiHTML(item.original)}<span>${htmlEscape(item.original)}</span></div>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -36,9 +37,9 @@ function makeCollections({mentions, emoji}) {
|
||||
menuItemTemplate: (item) => {
|
||||
return `
|
||||
<div class="tribute-item">
|
||||
<img src="${item.original.avatar}"/>
|
||||
<span class="name">${item.original.name}</span>
|
||||
${item.original.fullname && item.original.fullname !== '' ? `<span class="fullname">${item.original.fullname}</span>` : ''}
|
||||
<img src="${htmlEscape(item.original.avatar)}"/>
|
||||
<span class="name">${htmlEscape(item.original.name)}</span>
|
||||
${item.original.fullname && item.original.fullname !== '' ? `<span class="fullname">${htmlEscape(item.original.fullname)}</span>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -3428,4 +3428,16 @@ td.blob-excerpt {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
flex-wrap: wrap;
|
||||
|
||||
.comment-header-left {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.comment-header-right {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,8 @@
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
a.milestone {
|
||||
a.milestone,
|
||||
a.project {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
@@ -121,7 +122,7 @@
|
||||
|
||||
.branches {
|
||||
display: inline-flex;
|
||||
padding: 0 6px;
|
||||
padding: 0 4px;
|
||||
|
||||
.branch {
|
||||
background-color: var(--color-secondary);
|
||||
|
||||
Reference in New Issue
Block a user